home *** CD-ROM | disk | FTP | other *** search
Text File | 1994-09-04 | 143.7 KB | 3,347 lines |
- CHAPTER 11
-
- ACCESSING DOS AND BIOS SERVICES
-
-
- BASIC is arguably the most capable of all the popular high-level languages
- available for the PC. However, one area where all PC languages are weak is
- when accessing DOS and BIOS system interrupts. Previous chapters included
- subroutines and functions that access DOS interrupt services using CALL
- Interrupt, but in most cases with little explanation. This chapter
- explains what interrupts are, how they are accessed, and how they return
- information to your program.
- Only assembly language--the native language of the processor in every
- PC--can directly access interrupts. Assembly language programmers use the
- Int instruction, which transfers control to an *interrupt service routine*.
- An Int instruction is nearly identical to a conventional CALL statement,
- except a slightly different mechanism within the computer's hardware is
- used to implement it.
- BASIC lets you access system interrupts by providing a pair of
- assembly language interface routines called Interrupt and InterruptX.
- These routines accept the interrupt number and other parameters the
- interrupt requires, and they then perform the actual interrupt call.
- InterruptX is similar to Interrupt; the only real difference is that it
- lets you access two additional CPU registers.
-
-
- WHAT IS AN INTERRUPT?
- =====================
-
- The IBM PC family of personal computers supports two types of interrupts:
- hardware and software. A hardware interrupt is invoked by an external
- device or event, such as pressing a key on the keyboard. When this
- happens, a signal is sent from the keyboard hardware to the PC's
- microprocessor telling it to stop what it's currently doing and instead
- call one of the routines in the PC's BIOS.
- For example, while your PC is currently copying a group of files you
- may type DIR simultaneously, to display the results when the copying has
- finished. Even though DOS is reading and writing the files, you interrupt
- those operations for a few microseconds each time a key is pressed. The
- BIOS routine that handles the keyboard interrupt is responsible for placing
- the keystrokes into the PC's 15-character keyboard buffer. Then when DOS
- has finished copying your files, the DIR command will already be there.
- Because there is a direct physical connection between the keyboard
- circuitry and the PC's microprocessor, you are able to interrupt whatever
- else is happening at the time.
- A software interrupt, on the other hand, doesn't really interrupt
- anything. Rather, it is a form of CALL command that an assembly language
- program may issue. Just like the CALL command in BASIC that transfers
- control to a subroutine, a software interrupt is used in an assembly
- language program to access DOS and BIOS services. Although assembly
- language programs may use a CALL statement to invoke a subroutine, an
- interrupt instruction is needed to access the operating system routines.
- When a program issues a subroutine call, the address of that
- subroutine must be known, so the processor will be able to jump to the code
- there. With most programs, subroutine addresses are determined and
- assigned by LINK.EXE when it combines the various portions of your program
- into a single executable file. But this method can't be used with the DOS
- and BIOS routines, because their addresses are not known ahead of time.
- For example, if you compile a BASIC program on an IBM PC, it must also be
- able to be run on, say, a Tandy 1000 using a different version of DOS. Of
- course, it is impossible for LINK to know where the DOS and BIOS routines
- are located on the Tandy computer.
- To solve this problem and allow a program to call a routine whose
- address is not known, a list of addresses is stored in a known place in low
- memory. This place is called the *interrupt vector table*. The first
- 1,024 bytes in every PC contains a table of addresses for all 256 possible
- interrupts. Each table entry requires two words (four bytes): one word is
- used to hold the routine's segment, and the other holds its address within
- that segment. Whenever an assembly language program issues an interrupt
- instruction, the PC's processor automatically fetches the segment and
- address from this table, and then calls that address. Thus, any program
- may access any interrupt routine, without having to know where in memory
- the routine actually resides. The first four bytes in the interrupt vector
- table hold the address for Interrupt 0, the next four show where Interrupt
- 1 is, and so forth.
- DOS and BIOS services are specified by interrupt number, and most
- interrupt routines also expect a *service number*. Nearly all of the DOS
- services you will find useful are accessed through Interrupt &H21, with the
- desired service number specified in the AH register. In many cases,
- information is also returned in the CPU registers. For instance, the DOS
- service that returns the current default disk drive is specified by placing
- the value &H19 in the AH register. When the interrupt has finished, the
- current drive number is returned in the AL register. Registers will be
- described in the section that follows. As with the low memory addresses
- discussed in Chapter 10, the DOS and BIOS interrupt numbers use Hexadecimal
- numbering by convention.
- There are also several BIOS interrupts you will find useful, and these
- include video interrupt &H10, printer interrupt &H17, Print Screen
- interrupt 5, and the two equipment interrupts &H11 and &H12. There are
- other BIOS and DOS interrupts, but those are mostly useful when accessed
- from assembly language. For example, there is little need to call keyboard
- interrupt &H16 to read a key, since INKEY$ already does this. Likewise,
- you are unlikely to find disk interrupt &H13 very interesting, although it
- is used when performing copy protection and other low-level direct disk
- accesses. But unless you know what you are doing, it is possible--even
- likely--to trash your hard disk in the process of experimenting with this
- disk interrupt.
- I won't attempt to provide all of the information you need to access
- every possible DOS and BIOS service here. Indeed, a complete discussion
- would fill several books. Two excellent books that I recommend are "Peter
- Norton's Programmer's Guide to the IBM PC" (1988), and "Advanced MS-DOS",
- by Ray Duncan (1988). Both of these books are published by Microsoft
- Press, and can be found in most book stores. These books list every DOS
- and BIOS interrupt service, and show which registers are used to exchange
- information with each interrupt service.
- Also, once you have read and understood the information in this
- chapter you should go back to some of the examples presented in earlier
- chapters. In particular, Chapter 6 shows how to access DOS Interrupt &H21
- to read file names, and Chapter 7 includes routines that access Interrupt
- &H2F to see if a network is running on the host PC and if so which one.
-
-
- REGISTERS
- =========
-
- Microprocessors in the Intel 8086 family contain a set of built-in integer
- variables called *registers*. Each register can hold a single word (two
- bytes), which nicely corresponds to the size of a BASIC integer variable.
- Because these registers are contained within the microprocessor itself,
- they can be accessed by the CPU very quickly--much faster than variables
- which are stored in memory.
- The 8086 and 8088 microprocessors contain a total of fourteen
- registers. [Newer CPUs contain more registers, but they are not accessible
- via CALL Interrupt nor are they useful to a BASIC program.] Some of these
- registers are intended for a specific use, while others may be used as
- general purpose variables. For example, the CS and DS registers contain
- the current code and data segments respectively, while the CX register is
- often used as a counter in an assembly language FOR/NEXT loop. I'm not
- going to pursue a lengthy discussion of microprocessor theory here though,
- because it's not really necessary if you simply want to access a few system
- interrupts. Rather, I will focus on how to set up and invoke the various
- interrupt services, and interpret the results they return. Assembly
- language and CPU registers will be discussed more fully in Chapter 12.
- Both Interrupt and InterruptX (Interrupt Extended) require a TYPE
- variable with components that mirror each of the processor's registers.
- Figure 11-1 lists all of the 8086 registers that are accessible from BASIC,
- showing which are available with each of the interrupt routines.
-
-
- InterruptX Interrupt
- ========== =========
- AX AX
- BX BX
- CX CX
- DX DX
- BP BP
- SI SI
- DI DI
- Flags Flags
- DS
- ES
-
- Figure 11-1: The registers accessible from BASIC through Interrupt and
- InterruptX.
-
-
- When you call the either Interrupt routine, the values in a TYPE variable
- are copied into the CPU's registers, the interrupt is performed, and then
- the results returned in each register are copied back into a TYPE variable
- again. All of the CALL Interrupt examples Microsoft shows use two TYPE
- variables called InRegs and OutRegs. However, you can also use the same
- TYPE variable to both send and receive the register values. In fact, using
- a single TYPE variable will save a few bytes of DGROUP memory. Therefore,
- the remaining examples that use CALL Interrupt use a single TYPE variable.
- One important issue that needs to be addressed before we can proceed
- is how the CPU registers are accessed. I stated earlier that there are
- fourteen such registers, and each is the same size as an integer variable:
- 2 bytes. While this is certainly true, there is more to the story. Four
- of the registers--AX, BX, CX, and DX--can also be treated as being two
- separate one-byte registers.
- Each register half uses the designator "H" or "L" to mean High or Low.
- For example, the high-byte portion of AX is called AH, and the low-byte
- portion of CX is CL. When considered as a composite register, the two
- halves form a single integer word. Figure 11-2 shows how the AX register
- is constructed, with each half contributing to the total combined value.
-
-
- │<──────────────────── AX ─────────────────────>│
- ╔════════════════════════╤════════════════════════╗
- ║ 1 1 0 1 0 0 0 1 │ 1 1 0 0 1 1 0 1 ║
- ╚════════════════════════╧════════════════════════╝
- │<───────── AH ─────────>│<───────── AL ─────────>│
-
- Figure 11-2: How a single word-sized register may also be treated as two
- byte-sized registers.
-
-
- In an assembly language program it is simple to access each register half
- separately. However, BASIC does not offer a byte-sized variable type to
- use within the TYPE declaration. Therefore, a slight amount of math is
- required to get at each half separately. Although a fixed-length string
- with a length of one character could be used, the added overhead BASIC
- imposes to access a string as a number reduces the usefulness of that
- approach.
- Using Hexadecimal notation and multiplication simplifies access to
- each register half when it is being assigned, and integer division and
- BASIC's AND operator lets you separate the two halves when reading them.
- That is, you can assign the value &H12 to the upper byte in AH and the
- value &H34 to the lower byte in AL at one time, like this:
-
- Registers.AX = &H1234
-
- In many cases it is necessary to assign only AH, which can be done like
- this:
-
- Registers.AX = &H0600
-
- Here, the value 6 is placed into AH, and 0 is assigned to AL. Since many
- of the DOS and BIOS services ignore what is in AL, assigning a value of
- zero is the simplest and most effective solution. Again, using Hexadecimal
- notation lets you clearly define what is in each register half, because the
- first two digits represent the upper portion, and the second two represent
- the lower byte.
- When both the upper and lower bytes are important, you can use
- multiplication to assign them. By definition, any byte value in the high
- portion of a register is 256 times greater than it would be in the lower
- part. Thus, to assign the variable Low% to AL and High% to AH is as simple
- as this:
-
- Registers.AX = Low% + (256 * High%)
-
- In practice the parentheses are not really necessary because multiplication
- is always performed before addition. But I included them here for clarity.
- When an interrupt routine returns information in one of the
- combination registers, you may easily isolate the high and low portions as
- follows:
-
- Low% = Registers.DX AND 255
- High% = Registers.DX \ 256
-
- Some examples you may have seen use MOD to extract the lower byte, and that
- will also work:
-
- Low% = Registers.DX MOD 256
-
- Although MOD and AND cause BASIC to generate the same amount of assembly
- language code (three bytes), I generally prefer using AND because that
- instruction is somewhat faster on the older 8088 processors.
-
-
- ACCESSING THE BIOS
- ==================
-
- The simplest BIOS interrupt to call is the Print Screen interrupt,
- Interrupt 5. No parameters are required by this interrupt, and no values
- are returned when it finishes. But since the Interrupt routine expects the
- TYPE variable to be present and copies data to it, you must still dimension
- it in your program.
- Because Interrupt and InterruptX are external subroutines as opposed
- to built-in commands, you will need to load the Quick Library containing
- these routines. QuickBASIC comes with the file QB.QLB; BASIC PDS provides
- the same routines in a library named QBX.QLB. [And in VB/DOS this file is
- called VBDOS.QLB.] You must of course use whichever is appropriate for
- your version of BASIC. To start QuickBASIC and load the Quick Library that
- contains these routines use the /L switch like this:
-
- qb /l
-
- Normally, the name of a Quick Library must be given after the /L switch.
- However, QB and QBX know that /L by itself means to load the default QB.QLB
- or QBX.QLB Quick Library.
- The following complete program prints a simple pattern on the screen,
- and then sends it to the printer designated as LPT1: as if the PrtSc key
- had been pressed.
-
-
- DEFINT A-Z
- TYPE RegType
- AX AS INTEGER
- BX AS INTEGER
- CX AS INTEGER
- DX AS INTEGER
- BP AS INTEGER
- SI AS INTEGER
- DI AS INTEGER
- Flags AS INTEGER
- END TYPE
- DIM Registers AS RegType
-
- CLS
- FOR X% = 1 TO 24
- PRINT STRING$(80, X% + 64);
- NEXT
- CALL Interrupt(5, Registers, Registers)
-
-
- Although the Registers TYPE definition is shown here, the remaining
- examples in this chapter will instead specify the REGTYPE.BI include file
- that contains this code. QuickBASIC includes a similar include file called
- QB.BI, and BASIC PDS uses the name QBX.BI for the same file. [I created
- REGTYPE.BI so all of the programs in this book will run as is with any
- version of BASIC. But the BASIC-supplied versions also include DECLARE
- statements for the Interrupt routines, where my REGTYPE.BI file does not.
- Since all of these programs use the CALL keyword, a declaration is not
- strictly necessary.]
-
-
- THE BIOS VIDEO INTERRUPT
-
- The next example shows how to call BIOS video interrupt &H10 to clear just
- a portion of the display screen. It is designed as a combination
- demonstration and subprogram, so you can extract just the subprogram and
- add it to programs of your own.
-
-
- DEFINT A-Z
- DECLARE SUB ClearScreen (ULRow, ULCol, LRRow, LRCol, Colr)
-
- '$INCLUDE: 'REGTYPE.BI'
- DIM SHARED Registers AS RegType
-
- CLS
- FG = 7: BG = 1 'set the foreground and background colors
- COLOR FG, BG
-
- FOR X% = 1 TO 24
- PRINT STRING$(80, X% + 64);
- NEXT
-
- Colr = FG + 16 * BG 'use the same colors for clearing
- CALL ClearScreen(5, 10, 20, 70, Colr)
-
- SUB ClearScreen (ULRow, ULCol, LRRow, LRCol, Colr) STATIC
- Registers.AX = &H600
- Registers.BX = Colr * 256
- Registers.CX = (ULCol - 1) + (256 * (ULRow - 1))
- Registers.DX = (LRCol - 1) + (256 * (LRRow - 1))
- CALL Interrupt(&H10, Registers, Registers)
- END SUB
-
-
- There are two important benefits to using the BIOS for a routine such as
- this. One is of course the reduced amount of code that is needed, when
- compared to manually looping through memory using POKE to clear each
- character position. The second is the BIOS is responsible for determining
- the type of monitor installed, to select the correct video segment.
- The demonstration portion of the program first clears the screen, and
- then creates a simple test pattern using a color of white on blue. Just
- before the call to ClearScreen, the correct Colr parameter is calculated
- based on the same foreground and background specified to BASIC. Where
- BASIC accepts separate foreground and background values, the BIOS requires
- a single composite color byte.
- The simplified formula used in this example will accommodate normal
- colors, but does not support adding 16 to the foreground to specify a
- flashing color. This next formula shows how to derive a single color byte
- while also honoring flashing:
-
- Colr = (FG AND 16) * 8 + ((BG AND 7) * 16) + (FG AND 15)
-
- ClearScreen is then called telling it to clear a rectangular portion of the
- screen that lies within the boundary specified by an upper-left corner at
- location 5, 10 to the lower-right corner at location 20, 70. The color
- value calculated earlier is also passed, so the white on blue color will be
- maintained even after the screen is cleared.
- Within ClearScreen, four of the CPU's registers are assigned to values
- needed by the BIOS video interrupt. The first statement specifies service
- 6 in AH, which tells the BIOS to scroll the screen. The number of rows to
- scroll is then placed into the AL register, which we've set to zero. This
- particular BIOS service recognizes zero as a special flag, which tells it
- to clear the screen rather than scroll it.
- Service 6 also expects the color to use for clearing in the BH
- register. As I explained earlier, multiplying by 256 is equivalent to
- assigning just the higher portion of an integer, so the statement
- Registers.BX = Colr * 256 is equivalent to placing the one byte that is
- actually used by the Colr variable into BH.
- The next two instructions take the upper left and lower right corner
- arguments, and place them into the appropriate registers. In this case,
- the upper left column is placed into CL and the upper left row in CH.
- Similarly, the lower right column goes into DL and the lower right row into
- DH. Even though BASIC considers screen rows and columns to be numbered
- beginning at 1, the BIOS routines assume these to be zero-based.
- Therefore, 1 is subtracted from the parameters before they are placed into
- each component of the Registers TYPE variable. Finally, BASIC's Interrupt
- routine is called specifying Interrupt number &H10.
- Note that the same BIOS interrupt service can also be used to scroll a
- rectangular portion of the screen. Indeed, this is the primary purpose of
- service 6. To scroll a portion of the screen up a certain number of lines,
- you will place the number of lines into AL:
-
- Registers.AX = NumLines + (6 * 256)
-
- Scrolling the screen downward is also possible, using service 7 like this:
-
- Registers.AX = NumLines + (7 * 256)
-
- Also note that the Registers TYPE variable was dimensioned to be shared.
- This allows it to be accessed from all of the subprograms in a single
- program. If Registers is dimensioned in many different subprograms and
- functions, then a new instance will be created, with each stealing 20 bytes
- of DGROUP memory. Beware, however, that this memory savings has the
- potential drawback of introducing subtle bugs due to the same variable
- being used by different services. Whatever register values remain after
- one use of CALL Interrupt will still be present the next time, unless new
- values are explicitly assigned. [But that is rarely a problem, since you
- will generally assign all of the registers that a given interrupt needs
- just before calling that interrupt.]
- Although this short example simply clears or scrolls a portion of the
- display screen, it provides a foundation for nearly anything else you may
- need to do using CALL Interrupt. The DOS interrupt examples that follow
- will build on this foundation, and show how to access a wealth of useful
- services that are not otherwise possible using BASIC alone.
-
-
- ACCESSING DOS INTERRUPTS
- ========================
-
- As with the BIOS video interrupt services, DOS interrupt &H21 expects a
- service number to be given in the AH register. Many DOS services require
- additional information in other registers as well, including integer values
- and the segments and addresses of variables.
- The DOS services that accept or return a string (such as a file or
- directory name) require the address of the string, to know where it is
- located. For example, the DOS service that changes the current directory
- is called with AH set to &H3B, and DS:DX holding the address of a string
- that contains the name of the directory to change to.
- Likewise, to obtain the current directory you would load AH with the
- value &H47, and DS:SI with the address of a string that will receive the
- current directory's name. It is essential that this string already be
- initialized to a sufficient length before calling DOS. Otherwise, the
- returned directory name will likely overwrite other existing data. [And if
- that data happens to be a BASIC string descriptor or back pointer you will
- likely crash the program and possibly even have to reboot the PC.]
- When a string is sent as a parameter to a DOS routine, it must be
- terminated with a CHR$(0), so DOS can tell where it ends. Likewise, when
- DOS returns a string to your program such as the current directory, it
- indicates the end with a CHR$(0). Therefore, it is up to your program to
- manually append a CHR$(0) to any file or directory names you pass to DOS.
- And when receiving a string from DOS, you must use INSTR to locate the
- CHR$(0) that marks the end, and keep only what precedes that character.
- I will start with some simple examples that access DOS Interrupt &H21,
- and proceed to more complex routines that pass and receive string data.
-
-
- ACCESSING THE DEFAULT DRIVE
-
- The first DOS example shows how to determine the current default drive, and
- it is designed as a DEF FN-style function. A function is a natural way to
- design a routine that returns information, as opposed to a called
- subprogram. Further, using a DEF FN-style function reduces the amount of
- code that BASIC generates, and also reduces the code needed each time the
- function is invoked.
-
-
- DEFINT A-Z
-
- '$INCLUDE: 'REGTYPE.BI'
- DIM Registers AS RegType
-
- DEF FnGetDrive%
- Registers.AX = &H1900
- CALL Interrupt(&H21, Registers, Registers)
- FnGetDrive% = (Registers.AX AND &HFF) + 65
- END DEF
-
- PRINT "The current default drive is "; CHR$(FnGetDrive%)
-
-
- Here, service number &H19 is assigned to the AH portion of AX prior to
- calling Interrupt &H21, and the value that DOS returns in AL indicates the
- current drive. For this service DOS uses 0 to indicate drive A, 1 for
- drive B, and so forth. Therefore, you use AND with the value &HFF (255) to
- keep just the low portion in AX. Once the DOS drive number has been
- isolated, the program adds 65 to adjust that to the equivalent ASCII
- character value.
- Setting a new default drive is just as easy as obtaining the current
- drive. Although BASIC PDS provides the CHDRIVE command to set a new drive
- as the current default, QuickBASIC does not. The ChDrive subprogram that
- follows affords the same functionality to QuickBASIC users, and it accepts
- a single letter to indicate which drive is to be made the new current
- default.
-
-
- DEFINT A-Z
- DECLARE SUB ChDrive (Drive$)
-
- '$INCLUDE: 'REGTYPE.BI'
-
- DIM SHARED Registers AS RegType
-
- INPUT "Enter the drive to make current: ", NewDrive$
- CALL ChDrive(NewDrive$)
-
- SUB ChDrive (Drive$) STATIC
- Registers.AX = &HE00
- Registers.DX = ASC(UCASE$(Drive$)) - 65
- CALL Interrupt(&H21, Registers, Registers)
- END SUB
-
-
- Now that you know how to set and get the current default drive, you can
- combine the two and create a function that tells if a given drive letter is
- valid. Many DOS services return the success or failure of an operation
- using the CPU's Carry flag. However, the service that sets a new drive is
- a notable exception. Therefore, to determine if a given drive letter is in
- fact valid requires more than simply trying to set the new drive, and then
- seeing if an error resulted.
- The only way to tell if a request to change the current drive was
- accepted is to make another call to get the current drive, thereby seeing
- if the original request took effect. The program that follows accepts a
- drive letter as a string, and returns True or False (-1 or 0) to indicate
- whether or not the drive is valid.
-
- DEFINT A-Z
- DECLARE SUB ChDrive (Drive$)
-
- '$INCLUDE: 'REGTYPE.BI'
-
- DIM SHARED Registers AS RegType
-
- DEF FnGetDrive%
- Registers.AX = &H1900
- CALL Interrupt(&H21, Registers, Registers)
- FnGetDrive% = (Registers.AX AND &HFF) + 65
- END DEF
-
- DEF FnDriveValid% (TestDrive$)
- STATIC Current 'local to this function
- Current = FnGetDrive% 'save the current drive
- FnDriveValid% = 0 'assume not valid
- CALL ChDrive(TestDrive$) 'try to set a new drive
- IF ASC(UCASE$(TestDrive$)) = FnGetDrive% THEN
- FnDriveValid% = -1 'they match so it's valid
- END IF
- CALL ChDrive(CHR$(Current)) 'either way restore it
- END DEF
-
- INPUT "Enter the drive to test for validity: ", Drive$
- IF FnDriveValid%(Drive$) THEN
- PRINT Drive$; " is a valid drive."
- ELSE
- PRINT "Sorry, drive "; Drive$; " is not valid."
- END IF
-
- SUB ChDrive (Drive$) STATIC
- Registers.AX = &HE00
- Registers.DX = ASC(UCASE$(Drive$)) - 65
- CALL Interrupt(&H21, Registers, Registers)
- END SUB
-
- The strategy used here is to first save the current default drive, and then
- set a new drive on a trial basis. If the current drive is the one that was
- just set, then the specified drive was indeed valid. In either case, the
- original drive must be restored.
-
-
- DETERMINING IF A FILE EXISTS
-
- Both of the DOS services we have considered so far use integer arguments to
- indicate the new drive, or which drive is the current default. The next
- example shows how to pass a BASIC string to a DOS service, which is
- somewhat more complicated. The situation is made worse by the far strings
- feature available in BASIC PDS. Therefore, be sure to observe the comment
- that shows how to replace SSEG with VARSEG for use with QuickBASIC.
- Chapter 6 showed an admittedly clunky way to determine if a file is
- present. The example given there attempted to open the specified file for
- random access, and then used LOF to see if the file had a length of zero.
- The problem with that method--besides requiring a lot of unnecessary DOS
- activity--is that it reports a file with a perfectly legal length of zero
- as not being present, and then deletes it!
- The FnFileExist function that follows is intended for use with BASIC
- PDS, and comments show how to change it for use with QuickBASIC. Please
- understand that PDS doesn't really need a File Exist function, since DIR$
- can be used for that purpose. The statement IF LEN(DIR$(FileSpec$)) THEN
- will quickly tell if a file is present. However, the point is to show how
- strings are passed to DOS, and for that purpose this example serves quite
- nicely.
-
- DEFINT A-Z
- '$INCLUDE: 'REGTYPE.BI'
-
- DIM Registers AS RegType
-
- TYPE DTA 'used by DOS services
- Reserved AS STRING * 21 'reserved for use by DOS
- Attribute AS STRING * 1 'the file's attribute
- FileTime AS STRING * 2 'the file's time
- FileDate AS STRING * 2 'the file's date
- FileSize AS LONG 'the file's size
- FileName AS STRING * 13 'the file's name
- END TYPE
- DIM DTAData AS DTA
-
- DEF FnFileExist% (Spec$)
- FnFileExist% = -1 'assume the file exists
-
- Registers.DX = VARPTR(DTAData) 'set a new DOS DTA
- Registers.DS = VARSEG(DTAData)
- Registers.AX = &H1A00
- CALL InterruptX(&H21, Registers, Registers)
-
- Spec$ = Spec$ + CHR$(0) 'DOS needs an ASCIIZ string
- Registers.AX = &H4E00 'find file name service
- Registers.CX = 39 'attribute for any file
- Registers.DX = SADD(Spec$) 'show where the spec is
- Registers.DS = SSEG(Spec$) 'use this with BASIC PDS
- 'Registers.DS = VARSEG(Spec$) 'use this with QuickBASIC
-
- CALL InterruptX(&H21, Registers, Registers)
- IF Registers.Flags AND 1 THEN FnFileExist% = 0
- END DEF
-
- INPUT "Enter a file name or specification: ", FileSpec$
- IF FnFileExist%(FileSpec$) THEN
- PRINT FileSpec$; " does exist"
- ELSE
- PRINT "Sorry, no files match "; FileSpec$
- END IF
-
- FnFileExist calls upon the DOS Find First service that searches a directory
- and attempts to locate the first file that matches a given specification
- template. Therefore, besides being able to see if ACCOUNTS.DAT or
- F:\UTILS\NU.EXE exist, you can also use the DOS wild cards. For example,
- given C:\QB45\*.BAS, FnFileExist will report if any files with a .BAS
- extension are in the \QB45 directory of drive C.
- As part of its directory searching mechanism, DOS requires a block of
- memory known as a Disk Transfer Area, or DTA for short. If a matching file
- name is found, DOS stores important information about the file there, where
- your program can read it. As you can see by examining the DTAType
- structure, this includes the file's name and extension, the date and time
- it was last written, to, its current size, and attribute. The 21-byte
- string at the beginning identified as Reserved holds sector numbers and
- other information, and is used by DOS for subsequent searches. This
- function doesn't use any of the information in the DTA; however, it must
- still be defined for use by DOS.
- You will notice that FnFileExist uses the InterruptX routine rather
- than Interrupt, and this is to provide support for use with BASIC PDS far
- strings. Two of the CPU's registers are used to hold the DS and ES data
- segment registers. When Interrupt is called, it simply leaves whatever is
- currently in DS and ES and then calls the interrupt. InterruptX, on the
- other hand, loads DS and ES from those components of the Registers TYPE
- variable, and those are the values the interrupt itself receives. Were
- FnFileExist limited to working with QuickBASIC [where all strings are in
- the DS segment], Interrupt would be sufficient and the added complication
- of using either VARSEG or SSEG could be avoided.
- Note that InterruptX can also be told to use the current value of DS
- for both DS and ES, when the calling program doesn't need or want to change
- them. This is specified by placing a value of -1 into either or both
- portions of the Registers TYPE variable. For example, the statement
- Registers.DS = -1 tells InterruptX not to assign DS before performing the
- interrupt. Otherwise, if Registers.DS were not assigned, DS would receive
- the value 0 which is incorrect for DOS services that receive a variable's
- address. In a similar manner, Registers.ES = -1 tells InterruptX to set ES
- to the current value of DS.
-
-
- THE CARRY FLAG
-
- The last item to note in this function is how the Carry flag is tested. As
- I mentioned earlier, many DOS services indicate the success or failure of
- an operation by either clearing or setting the CPU's Carry flag. This flag
- is held in one bit in the Flags register, and its primary purpose is to
- assist multi-word arithmetic in assembly language programs. But because
- the 80x86 provides single instructions that easily set and test this flag,
- the designers of DOS decided to use it as an error indicator.
- The Carry flag is stored in the lowest bit of the Flags register, and
- can therefore be tested using the AND instruction with a value of 1. If
- that bit is set, the result of the AND test will be one; otherwise it will
- be zero. Thus, the statement IF Registers.Flags AND 1 THEN will be true if
- the Carry flag is set, which indicates an error. In the case of DOS' Find
- First function this is not really an error in the strictest sense. But
- there is no need here to distinguish between, say, an invalid path name and
- the lack of any matching files. Either a match was found or it wasn't.
-
-
- IMPROVING ON INTERRUPT
- ======================
-
- Recall that Chapter 8 introduced the DOSInt routine which serves as a
- small-code replacement for BASIC's InterruptX routine. Although the
- reduction in code size gained by using DOSInt versus Interrupt or
- InterruptX is not dramatic, it can save several hundred bytes in a program
- that calls it many times. DOSInt is also somewhat easier to set up and
- use, because it requires only a single Registers argument.
- Of course, DOSInt is meant only for use with DOS Interrupt &H21, and
- it will not work with any other DOS or BIOS interrupt services. Because of
- the savings that DOSInt affords, the remaining DOS examples in this chapter
- will use DOSInt instead of Interrupt or InterruptX. Like InterruptX,
- DOSInt lets you access the DS and ES registers, and it also recognizes an
- incoming value of -1 to specify the current contents of DS.
-
-
- OBTAINING THE CURRENT DIRECTORY
-
- Where FnFileExist shows how to pass a BASIC string to a DOS interrupt
- service, the FnGetDir function following shows how to receive a string from
- DOS. Again, BASIC PDS users have the CURDIR$ function which reports the
- current directory, but most QuickBASIC programmers will find this function
- invaluable.
-
- DEFINT A-Z
- '$INCLUDE: 'REGTYPE.BI'
-
- DIM Registers AS RegType
-
- DEF FnGetDir$ (Drive$)
- STATIC Temp$, Drive, Zero 'local variables
-
- IF LEN(Drive$) THEN 'did they pass a drive?
- Drive = ASC(UCASE$(Drive$)) - 64
- ELSE
- Drive = 0
- END IF
-
- Temp$ = SPACE$(65) 'DOS stores the name here
-
- Registers.AX = &H4700 'get directory service
- Registers.DX = Drive 'the drive goes in DL
- Registers.SI = SADD(Temp$) 'show DOS where Temp$ is
- Registers.DS = SSEG(Temp$) 'use this with BASIC PDS
- 'Registers.DS = -1 'use this with QuickBASIC
-
- CALL DOSInt(Registers) 'call DOS
-
- IF Registers.Flags AND 1 THEN 'must be an invalid drive
- FnGetDir$ = ""
- ELSE
- Zero = INSTR(Temp$, CHR$(0)) 'find the zero byte
- FnGetDir$ = "\" + LEFT$(Temp$, Zero)
- END IF
- END DEF
-
- PRINT "Which drive? ";
- DO
- Drive$ = INKEY$
- LOOP UNTIL LEN(Drive$)
- PRINT
-
- Cur$ = FnGetDir$(Drive$)
- IF LEN(Cur$) THEN
- PRINT "The current directory is ";
- PRINT Drive$; ":"; FnGetDir$(Drive$)
- ELSE
- PRINT "Invalid drive"
- END IF
-
- PRINT "The current directory for the default drive is ";
- PRINT FnGetDir$("")
-
- The variables Temp$, Drive, and Zero are declared as STATIC to prevent them
- from conflicting with variables of the same name in your program. Of
- course, you could convert this to a formal FUNCTION procedure if you
- prefer, which considers variables local by default. Converting to a formal
- function is also needed if you plan to access it from multiple source
- modules.
- Unlike the DOS Get Drive and Set Drive services, service &H47 uses a
- value of one to indicate drive A, 2 for drive B, and so forth. To request
- the current directory on the default drive you must use a value of zero.
- An explicit test for this is made at the beginning of the function. Later,
- this value is assigned to Registers.DX where DOS expects it. Note that it
- is really DL that will hold the specified drive number. But assigning DX
- from Drive as shown does this, and also clears the high (DH) portion in the
- process. Since the contents of DH are ignored by this DOS service, no harm
- is done and the extra code that would be needed to assign only DL can be
- avoided.
- As I mentioned earlier, it is essential that you set aside space to
- hold the returned directory name. Since the longest path name that DOS can
- accommodate is 65 characters, Temp$ is assigned to that length. Then, the
- segment and address where Temp$ is stored are passed to DOS in the DS and
- SI registers. Note that DOS is not very consistent in its use of
- registers. Where the service that finds the first matching file name uses
- DS:DX to point to the file specification, this service uses DS:SI to point
- to the string.
- Like the FnFileExist function, you must change the statement that
- assigns Registers.DS if you plan to use this one with QuickBASIC. The
- BASIC PDS version of that statement is left active rather than the
- QuickBASIC version, so QuickBASIC will highlight that line as an error to
- remind you. Although FnFileExist uses VARSEG for the DS value when used
- with QuickBASIC, FnGetDir uses -1. Both methods work, and I used -1 here
- just to show that in context.
- After DOSInt is called to load Temp$ with the current directory name,
- the Carry Flag is tested to see if an error occurred. The only error that
- is possible here is "Invalid drive", in which case FnGetDir$ is assigned a
- null value as a flag to indicate that. Otherwise, INSTR is used to locate
- the CHR$(0) zero byte that DOS assigned to mark the end of the name.
- This error testing can be left out to save code if you prefer. You
- could also validate the drive using the FnDriveValid function, either by
- adding the code within FnGetDir, or separately prior to invoking it.
-
-
- READING FILE AND DIRECTORY NAMES
-
- One important service that many programs need and which BASIC has never
- provided is the ability to read directory names from disk. Any word
- processor worth its salt will let you view a list of files that match, say,
- a *.DOC extension, and then select the one you want to edit. With the
- introduction of BASIC PDS Microsoft added the DIR$ function, which lets you
- read file names. However, there is no way to specify file attributes
- (hidden, read-only, and so forth), and also no way to read directory names.
- To add insult to injury, the PDS manuals do not show clearly how to read a
- list of file names, and store them into a string array.
- The program that follows counts the number of files or directories
- that match a given specification, and then dimensions and loads a string
- array with their names.
-
- DEFINT A-Z
- DECLARE SUB LoadNames (FileSpec$, Array$(), Attribute%)
-
- '$INCLUDE: 'REGTYPE.BI'
-
- TYPE DTA 'used by find first/next
- Reserved AS STRING * 21 'reserved for use by DOS
- Attribute AS STRING * 1 'the file's attribute
- FileTime AS STRING * 2 'the file's time
- FileDate AS STRING * 2 'the file's date
- FileSize AS LONG 'the file's size
- FileName AS STRING * 13 'the file's name
- END TYPE
-
- DIM SHARED DTAData AS DTA 'shared so LoadNames can
- DIM SHARED Registers AS RegType ' access them too
-
-
- DEF FnFileCount% (Spec$, Attribute)
- STATIC Count 'make this private
-
- Registers.DX = VARPTR(DTAData) 'set new DTA address
- Registers.DS = -1 'the DTA is in DGROUP
- Registers.AX = &H1A00 'specify service 1Ah
- CALL DOSInt(Registers) 'DOS set DTA service
-
- Count = 0 'clear the counter
- Spec$ = Spec$ + CHR$(0) 'make an ASCIIZ string
- IF Attribute AND 16 THEN 'find directory names?
- DirFlag = -1 'yes
- ELSE
- DirFlag = 0 'no
- END IF
-
- Registers.DX = SADD(Spec$) 'the file spec address
- Registers.DS = SSEG(Spec$) 'this is for BASIC PDS
- 'Registers.DS = -1 'this is for QuickBASIC
- Registers.CX = Attribute 'assign the attribute
- Registers.AX = &H4E00 'find first matching name
-
- DO
- CALL DOSInt(Registers) 'see if there's a match
- IF Registers.Flags AND 1 THEN EXIT DO 'no more
- IF DirFlag THEN
- IF ASC(DTAData.Attribute) AND 16 THEN
- IF LEFT$(DTAData.FileName, 1) <> "." THEN
- Count = Count + 1 'increment the counter
- END IF
- END IF
- ELSE
- Count = Count + 1 'they want regular files
- END IF
-
- Registers.AX = &H4F00 'find next name
- LOOP
-
- FnFileCount% = Count 'assign the function
- END DEF
-
-
- REDIM Names$(1 TO 1) 'create a dynamic array
- Attribute = 19 'matches directories only
- Attribute = 39 'matches all files
-
- INPUT "Enter a file specification: ", Spec$
- CALL LoadNames(Spec$, Names$(), Attribute)
-
- FOR X = LEN(Spec$) TO 1 STEP -1 'isolate the drive/path
- Temp = ASC(MID$(Spec$, X, 1))
- IF Temp = 58 OR Temp = 92 THEN '":" or "\"
- Path$ = LEFT$(Spec$, X) 'keep what precedes that
- EXIT FOR 'and we're all done
- END IF
- NEXT
-
- FOR X = 1 TO UBOUND(Names$) 'print the names
- PRINT Path$; Names$(X)
- NEXT
-
- PRINT
- PRINT UBOUND(Names$); "matching file(s)"
- END
-
-
- SUB LoadNames (FileSpec$, Array$(), Attribute) STATIC
-
- Spec$ = FileSpec$ + CHR$(0) 'make an ASCIIZ string
- NumFiles = FnFileCount%(Spec$, Attribute) 'count names
- IF NumFiles = 0 THEN EXIT SUB 'exit if none
- REDIM Array$(1 TO NumFiles) 'dimension the array
-
- IF Attribute AND 16 THEN 'find directory names?
- DirFlag = -1 'yes
- ELSE
- DirFlag = 0 'no
- END IF
-
- '---- The following code isn't strictly necessary
- ' because we know that FnFileCount already set the
- ' DTA address.
- 'Registers.DX = VARPTR(DTAData) 'set new DTA address
- 'Registers.DS = -1 'the DTA in DGROUP
- 'Registers.AX = &H1A00 'specify service 1Ah
- 'CALL DOSInt(Registers) 'DOS set DTA service
-
- Registers.DX = SADD(Spec$) 'the file spec address
- Registers.DS = SSEG(Spec$) 'this is for BASIC PDS
- 'Registers.DS = -1 'this is for QuickBASIC
- Registers.CX = Attribute 'assign the attribute
- Registers.AX = &H4E00 'find first matching name
- Count = 0 'clear the counter
-
- DO
- CALL DOSInt(Registers) 'see if there's a match
- IF Registers.Flags AND 1 THEN EXIT DO 'no more
- Valid = 0
- IF DirFlag THEN 'directories?
- IF ASC(DTAData.Attribute) AND 16 THEN
- IF LEFT$(DTAData.FileName, 1) <> "." THEN
- Valid = -1 'this name is valid
- END IF
- END IF
- ELSE
- Valid = -1 'they want regular files
- END IF
-
- IF Valid THEN 'process the file if it
- Count = Count + 1 ' passed all the tests
- Zero = INSTR(DTAData.FileName, CHR$(0))
- Array$(Count) = LEFT$(DTAData.FileName, Zero - 1)
- END IF
- Registers.AX = &H4F00 'find next matching name
- LOOP
-
- END SUB
-
- These routines call upon the DOS Find First and Find Next services, which
- performs the actual searching and loading of the names. Before the names
- can be loaded into an array, you need some way to know how many files there
- are. Therefore, the FnFileCount function makes repeated calls to DOS to
- find another file, until there are no more.
- The general strategy is to request service &H4E to find the first
- matching file. If a file is found then the Carry Flag is returned clear;
- otherwise it is set and the function returns with a count of zero. If a
- file is found Registers.AX is assigned a value of &H4F, and this tells DOS
- to resume searching based on the same file specification as before. Where
- the FnFileExist function merely needed to check for the presence of a file
- using the Find First service, this one continues in a DO loop until no more
- matching files are found.
- Understand that these DOS services accept either a partial file
- specification such as "*.BAS" or "D:\PATHNAME\*.*", or a single file name
- such as "CONFIG.SYS" or "C:\AUTOEXEC.BAT".
-
-
- File Attributes
-
- The DOS Find services also accept--and require--a file attribute indicating
- the type of files that are being sought. The method of specifying and
- isolating files and their attributes is convoluted and confusing to be
- sure. Figure 11-3 lists each of the six file attributes, and shows which
- corresponds to each bit in the attribute byte.
-
-
- 7 6 5 4 3 2 1 0 <── Bits
- 128 64 32 16 8 4 2 1 <── Numeric Values
- ═══ ═══ ═══ ═══ ═══ ═══ ═══ ═══
- │ │ │ │ │ │ │ │
- │ │ │ │ │ │ │ └───── Read-Only
- │ │ │ │ │ │ └───────── Hidden
- │ │ │ │ │ └───────────── System
- │ │ │ │ └───────────────── Volume Label
- │ │ │ └───────────────────── Subdirectory
- │ │ └───────────────────────── Archive
- └───┴───────────────────────────── Unused
-
- Figure 11-3: The makeup of the bits in the attribute byte, and the
- individual decimal value of each.
-
-
- In most cases, the attribute bits are cumulative. For example, if you
- specify that you want to locate files marked as read-only, you will also
- get files that are not. But if you leave that bit clear, then read-only
- files will not be included. The same logic is used for reading directory
- names. If the directory bit is set then you will read directories, and
- also regular files whose directory bit is not set. This requires that you
- perform additional qualifications when the file name is read into the DTA.
- To make matters even worse, there is an exception to this rule whereby an
- attribute of zero will still read file names whose archive bit is set.
- Before considering how to qualify the names as they are read, you must
- first understand what attributes are and how to specify them to begin with.
- Every file has an attribute, which is set by DOS to Archive at the time it
- is created. The archive bit is used solely to tell if the file has been
- backed up using the DOS BACKUP utility. When BACKUP copies the file to a
- floppy disk, it clears the Archive bit in the file's directory entry. Then
- if the file is written to again later, DOS sets that bit. This way, BACKUP
- can tell which files need to be backed up, and which ones haven't changed
- since the last backup was performed. Most modern commercial backup
- utilities also manipulate the archive bit, for the same reason that DOS'
- BACKUP does.
- The hidden bit tells the DOS DIR command not to display that file's
- name. Although it won't display in a directory listing, a hidden file may
- be opened, read from, and written to. The system bit is similar in that it
- also tells DIR not to display the file. The IO.SYS and MSDOS.SYS files
- that come with MS-DOS are hidden system files, so to read their names you
- must set those bits in the search attribute. Note that IBM's version of
- DOS uses the names IBMBIO.COM and IBMDOS.COM respectively for the same
- files.
- The label bit identifies a file as the disk's volume label, which
- isn't really a file at all. Every disk is allowed to have one volume label
- entry in its root directory, which lets an application identify the disk.
- This feature is not particularly important with hard disks, but when
- floppy-only systems were the norm this let programs ensure that the correct
- data diskette was installed in the drive. Even though a volume label is
- stored in the disk's directory like a regular file name, no sectors are
- allocated to it. Note that a bug in DOS 2.x versions causes a search for a
- volume label to fail. The only work-around is to use the more complex DOS
- 1.x Find First/Next services that are still supported in later versions for
- compatibility with older programs.
- Finally, the subdirectory attribute bit identifies a file as a
- directory. From DOS' perspective a subdirectory *is* a file, with fixed-
- length records that hold the names, attributes, and other information for
- the files it contains. Notice that the "." and ".." directory entries that
- appear when you type DIR are in fact present in that directory.
- Every directory except the root contains these entries, and they also
- have a directory attribute. The single dot refers to the current
- directory, and the double dots to the parent directory one level above. I
- mention this because these "dot" entries are reported by the Find First and
- Find Next services, and in many cases you will want to filter them out.
- To specify a file attribute you must determine the correct value,
- based on the individual bits to be included in the search. As I stated
- earlier, setting the attribute to zero includes all normal files, and
- exclude any marked as read-only, hidden, system, or subdirectory.
- Therefore, to include all files but not subdirectories you will use an
- attribute value of 39. This value is derived by adding up the bit values
- for each desired attribute as shown in Figure 11-3.
- When you add all of the values for each bit of interest, the answer is
- 32 (archive) + 4 (system) + 2 (hidden) + 1 (read-only) = 39. In a similar
- fashion, you will use 16 to read directory names, but hidden or read-only
- directories will not be included unless you also add 2 + 1 = 3, resulting
- in a final value of 19.
- Although you can specify attribute bits in nearly any combination, DOS
- returns all of the names that match any of the bits. Therefore, you must
- further qualify the files by examining the attribute DOS returns in the DTA
- TYPE variable. A typical search for directory names will ask to include
- all three attribute bits (directory, hidden, and read-only), but the
- qualification test merely tests if the directory bit is set. The following
- excerpt shows this in context.
-
- Registers.CX = 19
- CALL DOSInt(Registers)
- IF ASC(DTAData.Attribute) AND 16 THEN 'it is a directory
-
- Even if the directory was in fact hidden or read-only, the test for the
- directory bit will succeed regardless of any other bits that may be set.
- Unfortunately, the reverse is not true. If the directory is not hidden or
- read-only, then testing for those bits will fail. Both the FnFileCount
- function and the LoadNames subprogram include an explicit test for
- directory searches, and contain additional logic to check for this case.
- You could also add similar logic to the FnFileExist function, or
- create a separate version perhaps called FnDirExist that adds a test for
- the directory bit and also filters out the "dot" entries.
-
-
- REDIM PRESERVE
-
- One glaring shortcoming you have probably already noticed is the enormous
- amount of code that is duplicated in both the FnFileCount and LoadNames
- routines. In fact, the two are almost identical, except that LoadNames
- also assigns elements in the array. Worse, having to count all of the
- names before they can be read greatly increases the amount of time needed
- to process a directory when there are many files. Until you know how many
- files are present, there's no way to known how large to dimension the
- string array.
- One solution is to create an array with, say, 500 elements, and hope
- that the actual number of files does not exceed that. But if there are
- only a few files this wastes a lot of memory, and when there are more than
- 500, then, well, you're still out of luck. In fact, this is one of the few
- features that C offers but QuickBASIC does not. C programs can allocate
- memory that will be treated as an array, and then repeatedly request more
- memory for that same array as it is needed.
- Fortunately, BASIC PDS version 7.1 includes the PRESERVE option to the
- REDIM statement. This allows you to increase (or decrease) the size of an
- array, but without destroying its current contents. Thus, REDIM PRESERVE
- is ideal for applications like this that require an array's size to be
- altered. The next, much shorter program uses REDIM PRESERVE to advantage,
- and avoids the extra step that counts how many files match the search
- specification. Of course, this program requires BASIC PDS.
-
- DEFINT A-Z
- DECLARE SUB LoadNames (FileSpec$, Array$(), Attribute%)
-
- '$INCLUDE: 'REGTYPE.BI'
-
- TYPE DTA 'used by find first/next
- Reserved AS STRING * 21 'reserved for use by DOS
- Attribute AS STRING * 1 'the file's attribute
- FileTime AS STRING * 2 'the file's time
- FileDate AS STRING * 2 'the file's date
- FileSize AS LONG 'the file's size
- FileName AS STRING * 13 'the file's name
- END TYPE
-
- DIM SHARED DTAData AS DTA 'shared so LoadNames can
- DIM SHARED Registers AS RegType ' access them too
-
- REDIM Names$(1 TO 1) 'create a dynamic array
- Attribute = 19 'matches directories only
- Attribute = 39 'matches all files
- Spec$ = "*.*" 'so does this
- CALL LoadNames(Spec$, Names$(), Attribute)
-
- IF Names$(1) = "" THEN 'check for no files
- PRINT "No matching files"
- ELSE
- FOR X = 1 TO UBOUND(Names$) 'print the names
- PRINT Path$; Names$(X)
- NEXT
- END IF
- END
-
-
- SUB LoadNames (FileSpec$, Array$(), Attribute) STATIC
- Spec$ = FileSpec$ + CHR$(0) 'make an ASCIIZ string
- Count = 0 'clear the counter
-
- Registers.DX = VARPTR(DTAData) 'set new DTA address
- Registers.DS = -1 'the DTA is in DGROUP
- Registers.AX = &H1A00 'specify service 1Ah
- CALL DOSInt(Registers) 'DOS set DTA service
-
- IF Attribute AND 16 THEN 'find directory names?
- DirFlag = -1 'yes
- ELSE
- DirFlag = 0 'no
- END IF
-
- Registers.DX = SADD(Spec$) 'the file spec address
- Registers.DS = SSEG(Spec$) 'this is for BASIC PDS
- Registers.CX = Attribute 'assign the attribute
- Registers.AX = &H4E00 'find first matching name
-
- DO
- CALL DOSInt(Registers) 'see if there's a match
- IF Registers.Flags AND 1 THEN EXIT DO 'no more
-
- Valid = 0 'invalid until qualified
- IF DirFlag THEN 'find directories?
- IF ASC(DTAData.Attribute) AND 16 THEN 'yes, is it?
- IF LEFT$(DTAData.FileName, 1) <> "." THEN
- Valid = -1 'this name is valid
- END IF
- END IF
- ELSE
- Valid = -1 'they want regular files
- END IF
-
- IF Valid THEN 'process the file if it
- Count = Count + 1 ' passed all the tests
- REDIM PRESERVE Array$(1 TO Count) 'expand the array
- Zero = INSTR(DTAData.FileName, CHR$(0)) 'find zero
- Array$(Count) = LEFT$(DTAData.FileName, Zero - 1)
- END IF
-
- Registers.AX = &H4F00 'find next matching name
- LOOP
- END SUB
-
- MANAGING FILES
-
- Chapter 6 explained in great detail how files are opened, closed, read, and
- written using BASIC. I mentioned there that BASIC imposes a number of
- arbitrary limitations on what you can and cannot do with files. Indeed,
- DOS allows almost any action except writing to a file that has been opened
- for input. As you can imagine, CALL Interrupt--or in this case the DOSInt
- replacement routine--can be used to circumvent BASIC and access your files
- directly.
- Although BASIC expects you to state how the file will be accessed with
- the various OPEN options, to DOS all files are considered as being opened
- for binary access. There is no equivalent DOS service for BASIC's INPUT #
- or PRINT # commands. Therefore, it is up to you to write subroutines that
- look for a terminating carriage return and optional line feed when reading
- sequential text. Likewise, it is up to you to manually append a carriage
- return and line feed to the end of each line of text written to disk.
- Frankly, sequential file access is often best left to BASIC, since a
- lot of time-consuming tests are needed when reading sequential data. You
- could, however, use the BufIn function shown in Chapter 6, or similar logic
- of your own devising. There are many types of file access that can be
- performed using direct DOS calls, and I will show those that are the most
- useful and appropriate here.
- The program that will follow shortly is a combination demonstration,
- and suite of twelve subprograms and functions that perform most of the
- services necessary for manipulating files. Subprograms are provided to
- replace BASIC's OPEN, CLOSE, GET, and PUT statements, as well as LOCK and
- UNLOCK, SEEK, and KILL.
- There are also replacement functions for LOC and LOF, as well as two
- additional subprograms that have no BASIC equivalent. All of the routines
- use the DOSInt interface routine, and avoid using BASIC's file handling
- statements. The demonstration is comprised of a series of code blocks that
- exercise each routine showing how it is used. Comments at the start of
- each block explain what is being demonstrated.
- One reason to go behind BASIC's back this way is to avoid its many
- restrictions. For example, BASIC will not let you read from a file that
- has been opened for output, even though DOS considers this to be perfectly
- legal. Another is to avoid the need for ON ERROR. As you learned in
- Chapter 3, ON ERROR can make a program run more slowly, and also increase
- its size. By going directly to DOS you can avoid the burden of ON ERROR,
- which is otherwise needed to prevent your program from terminating if an
- error occurs. These replacement routines avoid errors such as those caused
- by attempting to open a file that does not exist, or trying to lock a
- network file that has already been locked by someone else.
- As with some of the other programs in this book that combine a
- demonstration and subroutines, you should make a copy of the file, and then
- delete all of the code in the main portion of the program. The only lines
- that must not be deleted are the DEFINT, DECLARE, and INCLUDE statements,
- and also the two DIM SHARED statements. Then, you can load the resultant
- module into the BASIC editor along with your own main application.
-
- 'DOS.BAS, demonstrates the direct DOS access routines
-
- DEFINT A-Z
- DECLARE FUNCTION DOSError% ()
- DECLARE FUNCTION ErrMessage$ (ErrNumber)
- DECLARE FUNCTION LocFile& (Handle)
- DECLARE FUNCTION LofFile& (Handle)
- DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
-
- DECLARE SUB ClipFile (Handle, NewLength&)
- DECLARE SUB CloseFile (Handle)
- DECLARE SUB FlushFile (Handle)
- DECLARE SUB KillFile (FileName$)
- DECLARE SUB LockFile (Handle, Location&, NumBytes&, Action)
- DECLARE SUB OpenFile (FileName$, OpenMethod, Handle)
- DECLARE SUB ReadFile (Handle, Segment, Address, NumBytes)
- DECLARE SUB SeekFile (Handle, Location&, SeekMethod)
- DECLARE SUB WriteFile (Handle, Segment, Address, NumBytes)
-
-
- '$INCLUDE: 'REGTYPE.BI'
-
- DIM SHARED Registers AS RegType 'so all can access it
- DIM SHARED ErrCode 'ditto for the ErrCode
- CRLF$ = CHR$(13) + CHR$(10) 'define this once now
-
- COLOR 15, 1 'this makes the DOS
- CLS 'messages high-intensity
- COLOR 7, 1
-
-
- '---- Open the test file we will use.
- FileName$ = "C:\MYFILE.DAT" 'specify the file name
- OpenMethod = 2 'read/write non-shared
- CALL OpenFile(FileName$, OpenMethod, Handle)
- GOSUB HandleErr
- PRINT FileName$; " successfully opened, handle:"; Handle
-
-
- '---- Write a test message string to the file.
- Msg$ = "This is a test message." + CRLF$
- Segment = SSEG(Msg$) 'use this with BASIC PDS
- 'Segment = VARSEG(Msg$) 'use this with QuickBASIC
- Address = SADD(Msg$)
- NumBytes = LEN(Msg$)
- CALL WriteFile(Handle, Segment, Address, NumBytes)
- GOSUB HandleErr
- PRINT "The test message was successfully written."
-
-
- '---- Show how to write a numeric value.
- IntData = 1234
- Segment = VARSEG(IntData)
- Address = VARPTR(IntData)
- NumBytes = 2
- CALL WriteFile(Handle, Segment, Address, NumBytes)
- GOSUB HandleErr
- PRINT "The integer variable was successfully written."
-
-
- '---- See how large the file is now.
- Length& = LofFile&(Handle)
- GOSUB HandleErr
- PRINT "The file is now"; Length&; "bytes long."
-
-
- '---- Seek back to the beginning of the file.
- Location& = 1 'specify file offset 1
- SeekMethod = 0 'relative to beginning
- CALL SeekFile(Handle, Location&, SeekMethod)
- GOSUB HandleErr
- PRINT "We successfully seeked back to the beginning."
-
-
- '---- Ensure that the Seek worked by seeing where we are.
- CurSeek& = LocFile&(Handle)
- GOSUB HandleErr
- PRINT "The DOS file pointer is now at location"; CurSeek&
-
-
- '---- Read the test message back in again.
- Buffer$ = SPACE$(23) 'the length of Msg$
- Segment = SSEG(Buffer$) 'use this with BASIC PDS
- 'Segment = VARSEG(Buffer$) 'use this with QuickBASIC
- Address = SADD(Buffer$)
- NumBytes = LEN(Buffer$)
- CALL ReadFile(Handle, Segment, Address, NumBytes)
- GOSUB HandleErr
- PRINT "Here is the test message: "; Buffer$
-
-
- '---- Skip over the CRLF by reading it as an integer.
- Address = VARPTR(Temp) 'read the CRLF into Temp
- Segment = VARSEG(Temp)
- NumBytes = 2
- CALL ReadFile(Handle, Segment, Address, NumBytes)
- GOSUB HandleErr
-
-
- '---- Read the integer written earlier, also into Temp.
- Address = VARPTR(Temp)
- Segment = VARSEG(Temp)
- NumBytes = 2
- CALL ReadFile(Handle, Segment, Address, NumBytes)
- GOSUB HandleErr
- PRINT "The integer value just read is:"; Temp
-
-
- '---- Append a new string at the end of the file.
- Msg$ = "This is appended to the end of the file." + CRLF$
- Segment = SSEG(Msg$) 'use this with BASIC PDS
- 'Segment = VARSEG(Msg$) 'use this with QuickBASIC
- Address = SADD(Msg$)
- NumBytes = LEN(Msg$)
- CALL WriteFile(Handle, Segment, Address, NumBytes)
- GOSUB HandleErr
- PRINT "The appended message has been written, ";
- PRINT "but it's still in the DOS file buffer."
-
-
- '---- Flush the file's DOS buffer to disk.
- CALL FlushFile(Handle)
- GOSUB HandleErr
- PRINT "Now the buffer has been flushed to disk. ";
- PRINT "Here's the file contents:"
- SHELL "TYPE " + FileName$
-
-
- '---- Display the current length of the file again.
- PRINT "Before calling ClipFile the file is now";
- Length& = LofFile&(Handle)
- GOSUB HandleErr
- PRINT Length&; "bytes long."
-
-
- '---- Clip the file to be 2 bytes shorter.
- NewLength& = LofFile&(Handle) - 2
- CALL ClipFile(Handle, NewLength&)
- PRINT "The file has been clipped successfully. ";
-
-
- '---- Prove that the clipping worked successfully.
- Length& = LofFile&(Handle)
- GOSUB HandleErr
- PRINT "It is now"; Length&; "bytes long."
-
-
- '---- Close the file.
- CALL CloseFile(Handle)
- GOSUB HandleErr
- PRINT "The file was successfully closed."
-
-
- '---- Open the file again, this time for shared access.
- OpenMethod = 66 'full sharing, read/write
- CALL OpenFile(FileName$, OpenMethod, Handle)
- GOSUB HandleErr
- PRINT FileName$; " successfully opened in shared mode";
- PRINT ", handle:"; Handle
-
-
- '---- Lock bytes 50 through 59.
- Start& = 50
- Length& = 10
- Action = 0 'specify locking
- CALL LockFile(Handle, Start&, Length&, Action)
- GOSUB HandleErr
- PRINT "File bytes 50 through 59 are successfully locked."
-
-
- '---- Prove that it is locked by asking DOS to copy it.
- PRINT "DOS (another process) fails to access the file:"
- SHELL "COPY " + FileName$ + " NUL"
-
-
- '---- Unlock the same range of bytes (mandatory).
- Start& = 50
- Length& = 10
- Action = 1 'specify unlocking
- CALL LockFile(Handle, Start&, Length&, Action)
- GOSUB HandleErr
- PRINT "File bytes 50 through 59 successfully unlocked."
-
-
- '---- Prove the unlocking worked by having DOS copy it.
- PRINT "Once unlocked DOS can access the file:";
- SHELL "COPY " + FileName$ + " NUL"
-
-
- CloseIt:
- '---- Close the file
- CALL CloseFile(Handle)
- GOSUB HandleErr
- PRINT "The file was successfully closed, ";
-
-
- '---- Kill the file to be polite
- CALL KillFile(FileName$)
- GOSUB HandleErr
- PRINT "and then successfully deleted."
-
- END
-
- '=======================================
- ' Error handler
- '=======================================
- HandleErr:
-
- TempErr = DOSError% 'call DOSError% just once
- IF TempErr = 0 THEN RETURN 'return if no errors
- PRINT ErrMessage$(TempErr) 'else print the message
- IF TempErr = 1 THEN 'we failed trying to lock
- COLOR 7 + 16
- PRINT "SHARE must be installed to continue."
- COLOR 7
- RETURN CloseIt
- ELSE 'otherwise end
- END
- END IF
-
-
- SUB ClipFile (Handle, Length&) STATIC
- '-- Use SeekFile to seek there, and then call WriteFile
- ' specifying zero bytes to truncate it at that point.
- ' Length& + 1 is needed because we need to seek just
- ' PAST the point where the file is to be truncated.
- CALL SeekFile(Handle, Length& + 1, Zero)
- IF ErrCode THEN EXIT SUB 'exit if an error occurred
- CALL WriteFile(Handle, Dummy, Dummy, Zero)
- END SUB
-
-
- SUB CloseFile (Handle) STATIC
- ErrCode = 0 'assume no errors
- Registers.AX = &H3E00 'close file service
- Registers.BX = Handle 'using this handle
- CALL DOSInt(Registers)
- IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
- END SUB
-
-
- FUNCTION DOSError%
- DOSError% = ErrCode 'simply return the error
- END FUNCTION
-
-
- FUNCTION ErrMessage$ (ErrNumber) STATIC
- SELECT CASE ErrNumber
- CASE 2
- ErrMessage$ = "File not found"
- CASE 3
- ErrMessage$ = "Path not found"
- CASE 4
- ErrMessage$ = "Too many files"
- CASE 5
- ErrMessage$ = "Access denied"
- CASE 6
- ErrMessage$ = "Invalid handle"
- CASE 61
- ErrMessage$ = "Disk full"
- CASE ELSE
- ErrMessage$ = "Undefined error: " + STR$(ErrNumber)
- END SELECT
- END FUNCTION
-
-
- SUB FlushFile (Handle) STATIC
- ErrCode = 0 'assume no errors
- Registers.AX = &H4500 'create duplicate handle
- Registers.BX = Handle 'based on this handle
-
- CALL DOSInt(Registers)
- IF Registers.Flags AND 1 THEN 'an error, assign it
- ErrCode = Registers.AX
- ELSE 'no error, so closing the
- TempHandle = Registers.AX 'dupe flushes the data
- CALL CloseFile(TempHandle)
- END IF
- END SUB
-
-
- SUB KillFile (FileName$) STATIC
- ErrCode = 0 'assume no errors
- LocalName$ = FileName$ + CHR$(0) 'make an ASCIIZ string
-
- Registers.AX = &H4100 'delete file service
- Registers.DX = SADD(LocalName$) 'using this handle
- Registers.DS = SSEG(LocalName$) 'use this with PDS
- 'Registers.DS = -1 'use this with QB
-
- CALL DOSInt(Registers)
- IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
- END SUB
-
-
- FUNCTION LocFile& (Handle) STATIC
- ErrCode = 0 'assume no errors
-
- Registers.AX = &H4201 'seek to where we are now
- Registers.BX = Handle 'using this handle
- Registers.CX = 0 'move zero bytes from here
- Registers.DX = 0
-
- CALL DOSInt(Registers)
- IF Registers.Flags AND 1 THEN 'an error occurred
- ErrCode = Registers.AX
- ELSE 'adjust to one-based
- LocFile& = (Registers.AX + (65536 * Registers.DX)) + 1
- END IF
- END FUNCTION
-
-
- SUB LockFile (Handle, Location&, NumBytes&, Action) STATIC
- ErrCode = 0 'assume no errors
- LocalLoc& = Location& - 1 'adjust to zero-based
-
- Registers.AX = Action + (256 * &H5C) 'lock/unlock
- Registers.BX = Handle
- Registers.CX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&) + 2)
- Registers.DX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&))
- Registers.SI = PeekWord%(VARSEG(NumBytes&), VARPTR(NumBytes&) + 2)
- Registers.DI = PeekWord%(VARSEG(NumBytes&), VARPTR(NumBytes&))
-
- CALL DOSInt(Registers)
- IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
- END SUB
-
-
- FUNCTION LofFile& (Handle)
- '---- first get and save the current file location
- CurLoc& = LocFile&(Handle) 'LocFile also clears ErrCode
- IF ErrCode THEN EXIT FUNCTION
-
- Registers.AX = &H4202 'seek to the end of the file
- Registers.BX = Handle 'using this handle
- Registers.CX = 0 'move zero bytes from there
- Registers.DX = 0
-
- CALL DOSInt(Registers)
- IF Registers.Flags AND 1 THEN 'an error occurred
- ErrCode = Registers.AX
- EXIT FUNCTION
- ELSE 'assign where we are
- LofFile& = Registers.AX + (65536 * Registers.DX)
- END IF
-
- Registers.AX = &H4200 'seek to where we were before
- Registers.BX = Handle 'using this handle
- Registers.CX = PeekWord%(VARSEG(CurLoc&), VARPTR(CurLoc&) + 2)
- Registers.DX = PeekWord%(VARSEG(CurLoc&), VARPTR(CurLoc&))
-
- CALL DOSInt(Registers)
- IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
- END FUNCTION
-
-
- SUB OpenFile (FileName$, Method, Handle) STATIC
- ErrCode = 0 'assume no errors
- Registers.AX = Method + (256 * &H3D) 'open file service
- LocalName$ = FileName$ + CHR$(0) 'make an ASCIIZ string
-
- DO
- Registers.DX = SADD(LocalName$) 'point to the name
- Registers.DS = SSEG(LocalName$) 'use this with PDS
- 'Registers.DS = -1 'use this w/QuickBASIC
-
- CALL DOSInt(Registers) 'call DOS
- IF (Registers.Flags AND 1) = 0 THEN 'no errors
- Handle = Registers.AX 'assign the handle
- EXIT SUB 'and we're all done
- END IF
-
- IF Registers.AX = 2 THEN 'File not found error
- Registers.AX = &H3C00 'so create it!
- ELSE
- ErrCode = Registers.AX 'read the code from AX
- EXIT SUB
- END IF
- LOOP
- END SUB
-
-
- SUB ReadFile (Handle, Segment, Address, NumBytes) STATIC
- ErrCode = 0 'assume no errors
-
- Registers.AX = &H3F00 'read from file service
- Registers.BX = Handle 'using this handle
- Registers.CX = NumBytes 'and this many bytes
- Registers.DX = Address 'read to this address
- Registers.DS = Segment 'and this segment
-
- CALL DOSInt(Registers)
- IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
- END SUB
-
-
- SUB SeekFile (Handle, Location&, Method) STATIC
- ErrCode = 0 'assume no errors
- LocalLoc& = Location& - 1 'adjust to zero-based
-
- Registers.AX = Method + (256 * &H42)
- Registers.BX = Handle
- Registers.CX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&) + 2)
- Registers.DX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&))
-
- CALL DOSInt(Registers)
- IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
- END SUB
-
-
- SUB WriteFile (Handle, Segment, Address, NumBytes) STATIC
- ErrCode = 0 'assume no errors
-
- Registers.AX = &H4000
- Registers.BX = Handle
- Registers.CX = NumBytes
- Registers.DX = Address
- Registers.DS = Segment
-
- CALL DOSInt(Registers)
- IF Registers.Flags AND 1 THEN
- ErrCode = Registers.AX
- ELSEIF Registers.AX <> Registers.CX THEN
- ErrCode = 61
- END IF
- END SUB
-
- This program begins by dimensioning two variables as SHARED throughout the
- entire module. By establishing the Registers TYPE variable as SHARED, all
- of the routines can use the same portion of DGROUP memory. If a separate
- DIM statement were used within each procedure, that many copies of this 20-
- byte variable would reside in memory at once. The CRLF$ variable does not
- need to be shared, because it is used only by the demonstration portion of
- the program.
- Before I describe each of these routines and how they are used, it is
- important to explain how DOS uses file handles. BASIC is unique among
- languages in that it allows you to make up an arbitrary file number that is
- used to access the files. With most languages and operating systems--and
- DOS is no exception--it is the operating system that assigns a number which
- your program must remember. Therefore, when you call the OpenFile routine
- to open a file, the Handle parameter is returned to you and you will use
- that number for subsequent file operations.
- Another important point is how errors are handled by these routines.
- Since you do not use ON ERROR to trap those situations another method is
- needed. Each routine clears or sets a global SHARED variable named
- ErrCode, which indicates its success or failure. After each call to one of
- these routines you will then check this variable, to see if it was
- successful. For the most efficiency, this program invokes a central error
- checking GOSUB routine that performs the actual testing. If an error
- occurs this routine prints an appropriate message using the ErrMessage$
- function, and then ends. The DOSError function is provided to allow access
- to ErrCode from other modules.
- In practice, it is not strictly necessary to add an explicit test
- after each subroutine call. For example, if you know the file has been
- opened successfully and you are sure the disk drive has sufficient space,
- then it is probably safe to assume that subsequent file writes will be
- okay. However, if you do call a routine that causes an error and don't
- check for that error, the next successful call to another routine will
- clear ErrCode and you will have no way to know about the earlier error.
-
-
- Opening a File
-
- The demonstration begins by first assigning a file name and open method,
- and then calling OpenFile to open the file. The open method lets you
- indicate the file access mode (reading, writing, or both), and also if the
- file will be accessed on a network. This parameter is bit-coded, and each
- bit has a parallel equivalent in BASIC's ACCESS READ, WRITE, SHARED, LOCK
- READ, and LOCK WRITE options. Figure 11-4 shows how these bits are
- organized.
-
-
- 7 6 5 4 3 2 1 0 <── Bits
- n/a 64 32 16 n/a 4 2 1 <── Numeric Values
- ═══ ═══ ═══ ═══ ═══ ═══ ═══ ═══
- │ │ │ │ │ │ │ │
- │ │ │ │ │ └───┴───┴───── Access Mode
- │ │ │ │ └───────────────── Reserved
- │ └───┴───┴───────────────────── Sharing Mode
- └───────────────────────────────── Inheritance
-
- Figure 11-4: The organization of the bits that establish how a file is to
- be opened.
-
-
- As with the file attribute bits shown earlier in Figure 11-3, you also need
- to set bits individually here to fully control the various file permission
- privileges. The access mode bits are valid with DOS versions 2.0 or later,
- and are equivalent to BASIC's ACCESS arguments. The sharing mode bits
- require DOS 3.0 or later, and also require SHARE.EXE to be installed. Note
- that some network software does not explicitly require SHARE, and provides
- the same functionality as part of its normal operation.
- The three lower bits control the file access, using the following
- binary code: 000 establishes read-only access, 001 allows writing only, and
- 010 allows both reading and writing. The term access as used here means
- what actions *your* program can perform, and has nothing to do with network
- or file sharing privileges.
- File sharing privileges are controlled by the three bits in the upper
- nybble (half-byte), and these determine what actions may be performed by
- other programs while your file is open. Regardless of what sharing (or
- locking) options you choose, your program always has full permission to
- access the file. The share bits are organized as follows: 000 means
- sharing is disabled, and this is what you must specify if you are not
- running on a network or when DOS 2.x is installed. A code of 001 denies
- other programs access to either read from or write to the file, 010 allows
- other programs to read but not write, and 011 allows writing but not
- reading. A code of 100 indicates full sharing, which lets other programs
- read and write, as long as that part of the file is not locked explicitly.
- Again, these codes are presented as binary values, and it is up to you
- to determine the correct value based on the settings of the individual
- bits. This is not as hard as it may sound at first, because you simply add
- up the bit values shown in the table. For example, to open a file for non-
- network read/write access under any version of DOS you use 000 + 010 = 2,
- which is the value used in the first OPEN example. To open a file for
- reading and writing and also allow other applications to access it fully
- you instead use 100 + 010 = 64 + 2 = 66. This is shown in the second OPEN
- statement. Figure 11-5 lists a few of the possible bit combinations, with
- the equivalent BASIC OPEN options.
-
-
- BASIC OPEN Statement Bits Value
- ================================= ======== =====
- OPEN FOR BINARY 00000010 2
- OPEN FOR BINARY ACCESS READ 00000000 0
- OPEN FOR BINARY ACCESS WRITE 00000001 1
- OPEN FOR BINARY ACCESS READ WRITE 00000010 2
- OPEN FOR BINARY ACCESS READ SHARED 01000000 64
- OPEN FOR BINARY LOCK READ 00110010 50
- OPEN FOR BINARY LOCK WRITE 00100010 34
-
- Figure 11-5: Bit equivalents for some of BASIC's OPEN options.
-
-
- Reading and Writing
-
- Once the file has been opened successfully, the next step is to show how to
- write a string variable in the same way BASIC does when you use PRINT #.
- The WriteFile and ReadFile routines each expect four arguments: the DOS
- file handle, the segment and address to save from or read into, and the
- number of bytes. These are the same parameters that DOS expects, and you
- can see by examining the subprograms that they merely pass this information
- on to DOS.
- Just before the first call to WriteFile, Msg$ is assigned a short test
- string, and a carriage return and line feed are appended to it manually.
- Remember, when you use BASIC's PRINT # command it is BASIC that adds these
- bytes for you. When dealing with DOS directly it is up to you to append
- these characters. Of course, you would omit these to mimic appending a
- semicolon at the end of a BASIC print line:
-
- PRINT #1, Msg$;
-
- SSEG then determines where the string data segment is, and SADD reports its
- address within that segment. The QuickBASIC version is shown as a comment,
- and it uses VARSEG instead. The number of bytes is obtained using LEN, and
- DOS accepts any value up to 65535. It is imperative that you never pass a
- value of zero for the number of bytes, or DOS will truncate the file at the
- current seek location. I will discuss this in more detail later on, in the
- section entitled *Beyond BASIC's File Handling*.
- The next example that writes an integer variable to the file is
- similar, except it uses a fixed length of 2. BASIC will not let you pass
- different types of data to one subprogram or function, which is why these
- read and write routines are designed to accept a segment and address.
- ReadFile is not called until later in the demonstration; however, it
- is nearly identical to WriteFile. Because you must tell ReadFile how many
- bytes are to be read, you should establish some type of system. One good
- one is the method used by Lotus and described in Chapter 6. For programs
- that do not need such a heavy-handed approach or that write only strings,
- you could use a simpler technique. For example, each string could be
- preceded by an integer length word, and that word would be read prior to
- reading each string. The short code fragment that follows shows how this
- might work.
-
- Segment = VARSEG(Length) 'Length is what gets read first
- Address = VARPTR(Length)
- CALL ReadFile(Handle, Segment, Address, 2)
-
- Work$ = SPACE$(Length) 'make a string that long
- Segment = SSEG(Work$) 'then read Length bytes into the string
- Address = SADD(Work$)
- CALL ReadFile(Handle, Segment, Address, Length)
-
-
- Setting and Reading the DOS Seek Location
-
- The LocFile and LofFile functions are similar to their BASIC LOC and LOF
- counterparts, except that LocFile is really equivalent to the SEEK
- function. Chapter 6 described the difference between the LOC and SEEK
- functions, and came to the inescapable conclusion that LOC is not nearly as
- useful as SEEK in most situations.
- The SeekFile subprogram, on the other hand, is equivalent to the
- statement form of BASIC's SEEK, and offers an interesting twist as an
- enhancement. Where BASIC's SEEK statement expects an offset from the
- beginning of the file, DOS provides additional seek methods. One lets you
- seek relative to where you are now in the file, and the other is relative
- to the end of the file. Therefore, I have included a SeekMethod parameter
- with my version of SeekFile, letting you enjoy the same flexibility.
- If SeekMethod is set to zero, DOS behaves the same as BASIC does and
- bases the new seek location from the beginning of the file. If SeekMethod
- is instead assigned to 1, the new offset into the file will be based on the
- current location. Note that you may use both positive *and* negative seek
- values, to move forward and backwards respectively. Finally, using a
- SeekMethod value of 2 tells DOS to consider the new location as being
- relative to the end of the file.
- For this method you may also use either a positive or negative value,
- to go beyond the end of the file or some offset before the end. While
- there is nothing inherently wrong with seeking past the end of a file, if
- any data is written at that point DOS will make that the new file length.
- And as explained in Chapter 6, the portion of the file that lies between
- the previous end of the file and the current end will hold whatever junk
- happened to be in the sectors that were just assigned to extend the length.
- One slight complication arises if you are dealing with fixed-length
- record data: you must calculate the appropriate file offset manually. The
- short one-line DEF FN function below shows how to do this.
-
- DEF FNSeekLoc&(RecNumber, RecLen) = ((RecNumber - 1) * CLNG(RecLen)) + 1
-
-
- Locking a File
-
- The LockFile subprogram serves the same purpose as BASIC's LOCK and UNLOCK
- statements. Because the code to lock and unlock a file are identical
- except for a single instruction, it seemed reasonable to combine the two
- services into one routine. LockFile expects four arguments: a handle, a
- starting offset, the number of bytes, and an action code. The starting
- offset and number of bytes use long integer values, to accommodate large
- files.
- Because DOS's Lock and Unlock services require you to specify the
- range of bytes to be locked, additional effort may be needed on your part.
- For example, if you are manipulating fixed-length records it is up to you
- to translate record numbers and record ranges to an equivalent binary
- offset and number of bytes. Fortunately, these values are very easy to
- determine using the following formulas:
-
- Location& = (RecNumber - 1) * CLNG(RecLength)
- NumBytes& = RecLength * CLNG(NumRecords)
-
- Note how CLNG is necessary to prevent BASIC from creating an overflow error
- if the result of the multiplications exceeds 32767.
- LockFile can also be used with normal BASIC file handling statements,
- if you merely want to avoid an error from attempting to lock a file that is
- already locked by another process. This requires you to use BASIC's
- FILEATTR function to obtain the equivalent DOS handle, thus:
-
- Handle = FILEATTR(FileNumber, 2)
-
- Here, FileNumber is the BASIC file number that was specified when the file
- was first opened. For example, if you used this:
-
- OPEN FileName$ FOR RANDOM SHARED AS #4 LEN = RecLength
-
- then the correct value for FileNumber will be 4.
-
-
- Beyond BASIC's File Handling
-
- Aside from SeekFile's ability to use the end of a file or the current seek
- location as a base point, the routines presented so far merely mimic the
- same capabilities BASIC already provides. Two notable exceptions, however,
- are ClipFile and FlushFile.
- The ClipFile subprogram lets you set a new length for a file, and that
- length may be either longer or shorter than the current length. ClipFile
- takes advantage of a little-known DOS feature that sets a new length for a
- file when you tell it to write zero bytes. This technique was used in the
- DBPACK.BAS program from Chapter 7, and it let that program remove deleted
- records from the end of a dBASE file.
- ClipFile begins by calling SeekFile to move the DOS file pointer just
- past the new length specified. If no error occurred it then calls
- WriteFile to write zero bytes at that point, thus establishing the new
- length. Notice the way the undefined variable Zero is used rather than a
- literal constant 0. As you already learned in Chapter 2, when a constant
- is passed to a subprogram or function, BASIC creates code to store a copy
- of the constant in DGROUP, and then passes the address of that copy.
- Although the variable Zero also requires two bytes of DGROUP memory for
- storage, the code to explicitly place the value there is avoided. Since an
- unassigned variable is always zero this method can be used with confidence.
- FlushFile also provides an important service that BASIC does not.
- When data is written to disk using either BASIC or DOS via direct interrupt
- calls, the last portion that was written is not necessarily on the physical
- disk. DOS buffers all file writes to minimize the number of disk accesses
- needed, thereby improving the speed of those writes. BASIC performs
- additional buffering as well, which further improves your program's
- performance. However, this creates a potential problem because a power
- outage or other disaster will cause any data in the file buffer to be lost.
- FlushFile calls upon another little-known DOS service called Duplicate
- Handle. When this service is called with the handle of a file that is
- already open, DOS creates a duplicate handle for the same file. This
- service is not that useful in and of itself, except for one important
- exception: When the duplicate handle is subsequently closed, DOS also
- writes the original file's contents to disk and updates the directory entry
- to reflect the current length. This is exactly what FlushFile does to
- flush the file buffer to disk.
-
-
- Error Messages
-
- The ErrMessage$ function is designed to display an appropriate message if
- an error occurs while using these routines. DOS has fewer error codes than
- BASIC, and it also uses a completely different numbering system. The
- ErrMessage$ function returns an error message that is equivalent to BASIC's
- where possible, but based on the DOS error return codes.
-
-
- Potential Problems
-
- Although this collection of file handling routines offers many improvements
- over using equivalent BASIC statements, there is one important issue I have
- not addressed here: handling critical errors. A critical error is caused
- by attempting to access a floppy disk drive with the drive door open, or no
- disk in place. At the DOS command line critical errors result in the
- infamous "Abort, Retry, Fail" message.
- Handling critical errors requires pure assembly language, and is a
- fairly complex undertaking. Therefore, I have purposely omitted that
- functionality from these routines. However, add-on library products such
- as QuickPak Professional and P.D.Q. from Crescent Software are written in
- assembly language, and include critical error handling.
- There is another potential problem you must be aware of when using
- these routines. When you open a file using BASIC's OPEN statement, and
- then restart the program before the file has been closed, BASIC closes the
- file before running your program again. This is done automatically and
- without your knowing about it.
- If you call OpenFile to open a file and then restart the program, the
- original file remains open. This causes no harm by itself--your program
- will simply receive the next available handle when it calls OpenFile. But
- at some point you will surely exhaust the available handles. The problem
- is that you will not be able to save your program, because the BASIC editor
- needs a handle when writing your source code to disk.
- The solution is to press F6 to go to the Immediate window, and then
- type the following line:
-
- FOR X% = 5 TO 20: CALL CloseFile(X%): NEXT
-
- This closes all of the files your program opened, thus freeing them for use
- by the BASIC editor. It is essential that you never close DOS handles zero
- through four, because they are in use by the PC. Since DOS uses these
- handles itself to print to the screen and read keyboard input, closing
- those handles will effectively lock up your PC. [Also, it is okay to close
- handles 5 through 20, even if your program hasn't opened that many. That
- is, asking DOS to close a file handle that was never opened does no harm.]
-
-
- ACCESSING THE MOUSE
- ===================
-
- All of the DOS and BIOS system services we have looked at so far rely on
- either the Interrupt routine that comes with BASIC, or the simplified
- DOSInt replacement. In a similar fashion, accessing the mouse driver also
- requires you to call interrupts. All of the mouse services are invoked
- using Interrupt &H33, and like DOS and the BIOS they require you to load
- the processor's registers to pass information, and then read them again
- afterward to obtain the results.
- In this section I will present several useful subroutines that show
- how to access the mouse interrupt. The first portion discusses the various
- utility routines, and shows how they are used. Following that, I will
- explain how the routines actually work and interface with the mouse driver.
-
-
- MOUSE SERVICES
-
- The important mouse services provided here are those that turn the mouse
- cursor on and off, position it on the screen and control its color, and let
- you determine which buttons are being pressed and where the cursor is
- presently located. Other routines show how to restrict the range of the
- mouse cursor's travel, and show how to define new, custom cursor shapes.
- To reduce the size of your programs I have written a short assembly
- language subroutine called MouseInt. This is similar to the DOSInt routine
- introduced in Chapter 6, except it is intended for use with the mouse
- interrupt &H33.
-
- ;MOUSEINT.ASM
-
- .Model Medium, Basic
-
- MouseRegs Struc
- RegAX DW ?
- RegBX DW ?
- RegCX DW ?
- RegDX DW ?
- Segmnt DW ?
- MouseRegs Ends
-
- .Code
-
- MouseInt Proc Uses SI DS ES, MRegs:Word
- Mov SI,MRegs ;get the address of MouseRegs
- Mov AX,[SI+RegAX] ;load each register in turn
- Mov BX,[SI+RegBX]
- Mov CX,[SI+RegCX]
- Mov DX,[SI+RegDX]
-
- Mov SI,[SI+Segmnt] ;see what the segment is
- Or SI,SI ;is it zero?
- Jz @F ;yes, skip ahead and use default
-
- Cmp SI,-1 ;is it -1?
- Je @F ;yes, skip ahead
- Mov DS,SI ;no, use the segment specified
-
- @@:
- Push DS ;either way, assign ES=DS
- Pop ES
- Int 33h ;call the mouse driver
-
- Push SS ;regain access to MouseRegs
- Pop DS
-
- Mov SI,MRegs ;access MouseRegs again
- Mov [SI+RegAX],AX ;save each register in turn
- Mov [SI+RegBX],BX
- Mov [SI+RegCX],CX
- Mov [SI+RegDX],DX
-
- Ret ;return to BASIC
- MouseInt Endp
- End
-
- Like DOSInt, this routine also uses a TYPE variable to define the various
- CPU registers that are needed by the mouse driver. However, fewer
- registers are needed simplifying the TYPE structure. You should define
- this TYPE variable as follows:
-
- TYPE MouseType
- AX AS INTEGER
- BX AS INTEGER
- CX AS INTEGER
- DX AS INTEGER
- Segment AS INTEGER
- END TYPE
- DIM MouseRegs AS MouseTYPE
-
- Since the mouse driver uses only these few registers, you can save a few
- bytes of DGROUP memory by using this subset TYPE instead of the full
- Registers TYPE that DOSInt requires. Notice the last component called
- Segment. Unlike the Mouse routine that Microsoft sells as an add-on
- library, MouseInt lets you specify a segment for passing far data to the
- mouse interrupt handler. For most mouse services you can leave the segment
- set to zero or -1. Either value tells MouseInt to use BASIC's default data
- segment. But some services that accept the address of incoming data also
- need to know the data's segment.
- In the Microsoft version you have no choice but to use static data and
- near memory arrays. Obviously, this precludes being able to use BASIC PDS
- far strings with that interface routine. You would instead have to create
- a single fixed-length string or TYPE variable, just to force the data to
- reside in near memory. When calling MouseInt with a value other than zero
- or -1 for the segment, MouseInt loads both DS and ES with that value.
- As with the collection of DOS file access routines, the following
- subprograms and functions can be added as a module to your program. Again,
- you should first make a copy of the source file that is included on the
- accompanying floppy disk, and then delete the demonstration portion of the
- program. This way, you can also run the original demonstration, and trace
- through it to test each of the mouse services. Of course, be sure to leave
- the commands that dimension the MouseRegs and MousePresent variables as
- being shared, and also the relevant DECLARE and DEFINT statements.
-
- 'MOUSE.BAS, demonstrates the various mouse services
-
- DEFINT A-Z
-
- '---- assembly language functions and subroutines
- DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
- DECLARE SUB MouseInt (MouseRegs AS ANY)
-
-
- '---- BASIC functions and subprograms
- DECLARE FUNCTION Bin2Hex% (Binary$)
- DECLARE FUNCTION MouseThere% ()
- DECLARE FUNCTION WaitButton% ()
- DECLARE SUB CursorShape (HotX, HotY, Shape())
- DECLARE SUB HideCursor ()
- DECLARE SUB MouseTrap (ULRow, ULCol, LRRow, LRCol)
- DECLARE SUB MoveCursor (X, Y)
- DECLARE SUB ReadCursor (X, Y, Buttons)
- DECLARE SUB ShowCursor ()
- DECLARE SUB TextCursor (FG, BG)
-
- DECLARE SUB Prompt (Message$) 'used for this demo only
-
-
- TYPE MouseType 'similar to DOS RegType
- AX AS INTEGER
- BX AS INTEGER
- CX AS INTEGER
- DX AS INTEGER
- Segment AS INTEGER
- END TYPE
-
- DIM SHARED MouseRegs AS MouseType
- DIM SHARED MousePresent
- REDIM Cursor(1 TO 32)
-
- IF NOT MouseThere% THEN 'ensure a mouse is present
- PRINT "No mouse is installed" ' and initialize it if so
- END
- END IF
- CLS
-
-
- DEF SEG = 0 'see what type of monitor
- IF PEEK(&H463) <> &HB4 THEN 'if it's color
- ColorMon = -1 'remember that for later
- SCREEN 12 'this requires a VGA
- LINE (0, 0)-(639, 460), 1, BF 'paint a blue background
- END IF
-
-
- DIM Choice$(1 TO 5) 'display some choices
- LOCATE 1, 1 'for something to point at
- FOR X = 1 TO 5
- READ Choice$(X)
- PRINT Choice$(X);
- LOCATE , X * 12
- NEXT
- DATA "Choice 1", "Choice 2", "Choice 3"
- DATA "Choice 4", "Choice 5"
-
-
- IF NOT ColorMon THEN 'if it's not color
- CALL TextCursor(-2, -2) 'select a text cursor
- END IF
-
-
- CALL ShowCursor
- CALL Prompt("Point the cursor at a choice, and press _
- a button.")
-
-
- DO 'wait for a button press
- CALL ReadCursor(X, Y, Button)
- LOOP UNTIL Button
- IF Button AND 4 THEN Button = 3 'for three-button mice
-
- CALL Prompt("You pressed button" + STR$(Button) + _
- " and the cursor was at location" + STR$(X) + "," + _
- STR$(Y) + " - press a button.")
-
- IF ColorMon THEN 'if it is a color monitor
- RESTORE Arrow ' load a custom arrow
- GOSUB DefineCursor
- END IF
- Dummy = WaitButton%
-
-
- IF ColorMon THEN 'the hardware can do it
- RESTORE CrossHairs 'set a cross-hairs cursor
- GOSUB DefineCursor
- CALL Prompt("Now the cursor is a cross-hairs, press _
- a button.")
- Dummy% = WaitButton%
- END IF
-
-
- IF ColorMon THEN 'now set an hour glass
- RESTORE HourGlass
- GOSUB DefineCursor
- END IF
-
-
- CALL Prompt("Now notice how the cursor range is _
- restricted. Press a button to end.")
- CALL MouseTrap(50, 50, 100, 100)
- Dummy = WaitButton%
-
- IF ColorMon THEN 'restore to 640 x 350
- CALL MouseTrap(0, 0, 349, 639)
- ELSE 'use CGA bounds for mono!
- CALL MouseTrap(0, 0, 199, 639)
- END IF
-
-
- Dummy = MouseThere% 'reset the mouse driver
- CALL HideCursor 'and turn off the cursor
- SCREEN 0 'revert to text mode
- END
-
-
- DefineCursor:
-
- FOR X = 1 TO 32 'read 32 words of data
- READ Dat$ 'read the data
- Cursor(X) = Bin2Hex%(Dat$) 'convert to integer
- NEXT
- CALL CursorShape(Zero, Zero, Cursor())
- RETURN
-
-
- Arrow:
-
- NOTES:
- 'The first group of binary data is the screen mask.
- 'The second group of binary data is the cursor mask.
- 'The cursor color is black where both masks are 0.
- 'The cursor color is XORed where both masks are 1.
- 'The color is clear where the screen mask is 1 and the
- ' cursor mask is 0.
- 'The color is white where the screen mask is 0 and the
- ' cursor mask is 1.
- '
- 'Mouse cursor designs by Phil Cramer.
-
- '--- this is the screen mask
- DATA "1110011111111111"
- DATA "1110001111111111"
- DATA "1110000111111111"
- DATA "1110000011111111"
- DATA "1110000001111111"
- DATA "1110000000111111"
- DATA "1110000000011111"
- DATA "1110000000001111"
- DATA "1110000000000111"
- DATA "1110000000000011"
- DATA "1110000000000001"
- DATA "1110000000011111"
- DATA "1110001000011111"
- DATA "1111111100001111"
- DATA "1111111100001111"
- DATA "1111111110001111"
-
- '---- this is the cursor mask
- DATA "0001100000000000"
- DATA "0001010000000000"
- DATA "0001001000000000"
- DATA "0001000100000000"
- DATA "0001000010000000"
- DATA "0001000001000000"
- DATA "0001000000100000"
- DATA "0001000000010000"
- DATA "0001000000001000"
- DATA "0001000000000100"
- DATA "0001000000111110"
- DATA "0001001100100000"
- DATA "0001110100100000"
- DATA "0000000010010000"
- DATA "0000000010010000"
- DATA "0000000001110000"
-
-
- CrossHairs:
-
- DATA "1111111101111111"
- DATA "1111111101111111"
- DATA "1111111101111111"
- DATA "1111000000000111"
- DATA "1111011101110111"
- DATA "1111011101110111"
- DATA "1111011111110111"
- DATA "1000000111000000"
- DATA "1111011111110111"
- DATA "1111011101110111"
- DATA "1111011101110111"
- DATA "1111000000000111"
- DATA "1111111101111111"
- DATA "1111111101111111"
- DATA "1111111101111111"
- DATA "1111111111111111"
-
- DATA "0000000010000000"
- DATA "0000000010000000"
- DATA "0000000010000000"
- DATA "0000111111111000"
- DATA "0000100010001000"
- DATA "0000100010001000"
- DATA "0000100000001000"
- DATA "0111111000111111"
- DATA "0000100000001000"
- DATA "0000100010001000"
- DATA "0000100010001000"
- DATA "0000111111111000"
- DATA "0000000010000000"
- DATA "0000000010000000"
- DATA "0000000010000000"
- DATA "0000000000000000"
-
-
- HourGlass:
-
- DATA "1100000000000111"
- DATA "1100000000000111"
- DATA "1100000000000111"
- DATA "1110000000001111"
- DATA "1110000000001111"
- DATA "1111000000011111"
- DATA "1111100000111111"
- DATA "1111110001111111"
- DATA "1111110001111111"
- DATA "1111100000111111"
- DATA "1111000000011111"
- DATA "1110000000001111"
- DATA "1110000000001111"
- DATA "1100000000000111"
- DATA "1100000000000111"
- DATA "1100000000000111"
-
- DATA "0000000000000000"
- DATA "0001111111110000"
- DATA "0000000000000000"
- DATA "0000111111100000"
- DATA "0000100110100000"
- DATA "0000010001000000"
- DATA "0000001010000000"
- DATA "0000000100000000"
- DATA "0000000100000000"
- DATA "0000001010000000"
- DATA "0000011111000000"
- DATA "0000110001100000"
- DATA "0000100000100000"
- DATA "0000000000000000"
- DATA "0001111111110000"
- DATA "0000000000000000"
-
-
- FUNCTION Bin2Hex% (Binary$) STATIC 'binary to integer
- Temp& = 0
- Count = 0
-
- FOR X = LEN(Binary$) TO 1 STEP -1
- IF MID$(Binary$, X, 1) = "1" THEN
- Temp& = Temp& + 2 ^ Count
- END IF
- Count = Count + 1
- NEXT
-
- IF Temp& > 32767 THEN Temp& = Temp& - 65536
- Bin2Hex% = Temp&
- END FUNCTION
-
-
- SUB CursorShape (HotX, HotY, Shape()) STATIC
- IF NOT MousePresent THEN EXIT SUB
-
- MouseRegs.AX = 9
- MouseRegs.BX = HotX
- MouseRegs.CX = HotY
- MouseRegs.DX = VARPTR(Shape(1))
- MouseRegs.Segment = VARSEG(Shape(1))
-
- CALL MouseInt(MouseRegs)
- END SUB
-
-
- SUB HideCursor STATIC 'turns off the mouse cursor
- IF NOT MousePresent THEN EXIT SUB
-
- MouseRegs.AX = 2
- CALL MouseInt(MouseRegs)
- END SUB
-
-
- FUNCTION MouseThere% STATIC 'reports if a mouse is present
- MouseThere% = 0 'assume there is no mouse
- IF PeekWord%(Zero, (4 * &H33) + 2) = 0 THEN 'segment = 0
- EXIT FUNCTION ' means there's no mouse
- END IF
-
- MouseRegs.AX = 0
- CALL MouseInt(MouseRegs)
- MouseThere% = MouseRegs.AX
- IF MouseRegs.AX THEN MousePresent = -1
- END FUNCTION
-
-
- SUB MouseTrap (ULRow, ULColumn, LRRow, LRColumn) STATIC
- IF NOT MousePresent THEN EXIT SUB
-
- MouseRegs.AX = 7 'restrict horizontal movement
- MouseRegs.CX = ULColumn
- MouseRegs.DX = LRColumn
- CALL MouseInt(MouseRegs)
-
- MouseRegs.AX = 8 'restrict vertical movement
- MouseRegs.CX = ULRow
- MouseRegs.DX = LRRow
- CALL MouseInt(MouseRegs)
- END SUB
-
-
- SUB MoveCursor (X, Y) STATIC 'positions the mouse cursor
- IF NOT MousePresent THEN EXIT SUB
-
- MouseRegs.AX = 4
- MouseRegs.CX = X
- MouseRegs.DX = Y
- CALL MouseInt(MouseRegs)
- END SUB
-
-
- SUB Prompt (Message$) STATIC 'prints prompt message
- V = CSRLIN 'save current cursor position
- H = POS(0)
- LOCATE 30, 1 'use 25 for EGA SCREEN 9
- CALL HideCursor 'this is very important!
- PRINT LEFT$(Message$, 79); TAB(80);
- CALL ShowCursor 'and so is this
- LOCATE V, H 'restore the cursor
- END SUB
-
-
- SUB ReadCursor (X, Y, Buttons) 'returns cursor and button
- ' information
- IF NOT MousePresent THEN EXIT SUB
-
- MouseRegs.AX = 3
- CALL MouseInt(MouseRegs)
-
- Buttons = MouseRegs.BX AND 7
- X = MouseRegs.CX
- Y = MouseRegs.DX
- END SUB
-
-
- SUB ShowCursor STATIC 'turns on the mouse cursor
- IF NOT MousePresent THEN EXIT SUB
-
- MouseRegs.AX = 1
- CALL MouseInt(MouseRegs)
- END SUB
-
-
- SUB TextCursor (FG, BG) STATIC
- IF NOT MousePresent THEN EXIT SUB
-
- MouseRegs.AX = 10
- MouseRegs.BX = 0
- MouseRegs.CX = &HFF
- MouseRegs.DX = 0
-
- IF FG = -1 THEN 'maintain FG as the cursor moves?
- MouseRegs.CX = MouseRegs.CX OR &HF00
- ELSEIF FG = -2 THEN 'invert FG as the cursor moves?
- MouseRegs.CX = MouseRegs.CX OR &H700
- MouseRegs.DX = &H700
- ELSE 'use the specified color
- MouseRegs.DX = 256 * (FG AND &HFF)
- END IF
-
- IF BG = -1 THEN 'maintain BG as the cursor moves?
- MouseRegs.CX = MouseRegs.CX OR &HF000
- ELSEIF BG = -2 THEN 'invert BG as the cursor moves?
- MouseRegs.CX = MouseRegs.CX OR &H7000
- MouseRegs.DX = MouseRegs.DX OR &H7000
- ELSE 'use the specified color
- Temp = (BG AND 7) * 16 * 256
- MouseRegs.DX = MouseRegs.DX OR Temp
- END IF
-
- CALL MouseInt(MouseRegs)
- END SUB
-
-
- FUNCTION WaitButton% STATIC 'waits for a button press
- IF NOT MousePresent THEN EXIT FUNCTION
-
- X! = TIMER 'pause to allow releasing
- WHILE X! + .2 > TIMER ' the button
- WEND
-
- DO 'wait for a button press
- CALL ReadCursor(X, Y, Button)
- LOOP UNTIL Button
-
- IF Button AND 4 THEN Button = 3 'for three-button mice
- WaitButton% = Button 'assign the function
- END FUNCTION
-
- This program begins by declaring all of the support functions, and then
- defines and dimensions the MouseRegs TYPE variable. The integer array is
- used to hold the custom graphics cursor shape information, which the
- CursorShape routine requires. The remainder of the program illustrates how
- to use the various mouse routines in your own programs.
-
-
- (2) Determining if a Mouse is Present
-
- The first function is MouseThere, which serves two important purposes:
- The first is to determine if a mouse is present. The second purpose of
- MouseThere is to initialize the mouse driver to its default parameters.
- This lets you be sure that the mouse color, shape, and other parameters are
- in a known state. Resetting the mouse is strongly recommended because some
- programs do not bother to reset the mouse when they are finished.
- Although there is a mouse service to determine if the driver is
- installed, you must also perform an additional test to prevent problems
- with early computers running DOS version 2. The problem arises because
- these computers leave the mouse interrupt (&H33) undefined if no mouse is
- present, and calling this interrupt is likely to make the PC crash.
- As you already know, the interrupt vector table in low memory holds
- the segment and address for every interrupt service routine that is present
- in the PC. But who puts those addresses into the interrupt vector table?
- All of the BIOS interrupt addresses are assigned by the BIOS as part of the
- power-up code in your PC's ROM. Likewise, DOS installs the addresses it
- needs while it is being loaded from disk.
- The BIOS in modern computers assigns every interrupt vector to a valid
- address, even those that it (the BIOS) does not use. The code pointed to
- by the unused interrupts is an assembly language Iret (Interrupt Return)
- instruction. So if no other routine is servicing that interrupt, calling
- it merely returns with no change to the register contents. But early
- computers and early versions of DOS ignored Interrupt &H33, and left the
- values in that vector address set to zero. [Calling the "code" at address
- zero is guaranteed to fail, since address zero holds other addresses and
- not executable code.] Therefore, to safely detect the presence of a mouse
- requires first looking in low memory, to ensure that the interrupt address
- there is valid.
- It is important to understand that you *must* use MouseThere once at
- the start of your program, before any of the other mouse routines will
- work. All of the mouse routines check the global variable MousePresent
- before calling MouseInt, and do nothing if it is zero. This safety
- mechanism lets you freely call the various mouse services without regard to
- whether or not a mouse is installed, to avoid the DOS 2 problem described
- earlier. Thus, the same program statements can accommodate a mouse if one
- is present or not, without requiring many separate IF tests.
- For example, you will probably want to write programs that use a mouse
- if one is present, but don't require it. If you had to have a separate
- block of code for each case, your program would be much larger and slower
- than necessary. Therefore, you can simply call these mouse routines
- whether or not a mouse is present. The code fragment that follows shows a
- simple example of this in context.
-
- PRINT "Press a key or mouse button to continue: ";
- DO
- Temp$ = INKEY$
- CALL ReadCursor(X, Y, Buttons)
- LOOP UNTIL LEN(INKEY$) OR Buttons
- PRINT "Thank you."
-
- If MouseThere determined that no mouse was present when it was called
- earlier, then ReadCursor will do nothing and return no values. Of course,
- you will have to check for mouse events and act on them, but these can be
- handled within the same blocks of code that also handle keyboard input.
- Once the program knows that a mouse is in fact present, it checks to
- see if the display adapter is color or monochrome. A color monitor
- supports more mouse options such as changing the shape of the mouse cursor.
- In this case the program assumes that you have a VGA adapter. If you have
- only an EGA, simply change the SCREEN 12 statement to SCREEN 9. You will
- also have to change the LOCATE command in the Prompt subprogram to use line
- 25 instead of line 30. Although the cursor shape can be altered with CGA
- and Hercules adapters, those are not accommodate here.
- Once the screen display mode is set, a filled box is drawn covering
- the entire screen, to create an attractive blue background. You should be
- aware that the drivers included come with many older, inexpensive clone
- mouse devices do not support the EGA and VGA display modes. This is not a
- limitation with the mouse hardware; rather, the problem lies in the driver
- software. Fortunately, the MOUSE.COM and MOUSE.SYS drivers that Microsoft
- includes with BASIC work with most brands of mouse. Furthermore, you are
- allowed to distribute those drivers with your own programs, as long as you
- include an appropriate copyright notice. See the license agreement that
- came with your version of BASIC for more information on displaying the
- Microsoft copyright.
-
-
- CONTROLLING THE TEXT CURSOR
-
- After reading and displaying a list of sample choices that serve as a menu,
- the program again checks to see which type of adapter is present. If it is
- monochrome, then a custom text cursor is defined using the TextCursor
- routine. This routine is appropriate for both monochrome and color
- adapters, and offers several useful options that let you control fully how
- the foreground and background colors will appear. Also, an initial call to
- TextCursor is needed with some non-Microsoft mouse drivers to ensure that
- the cursor is displayed after calling ShowCursor.
- TextCursor expects two parameters to control the cursor's foreground
- and background colors. If a positive value is given for either parameter,
- then that is the color the mouse cursor assumes as it travels around the
- screen. For example, if you use a color combination of 0, 4 the character
- under the mouse cursor will be shown in black on a red background. It is
- important to understand that the normal mouse cursor color is actually the
- character's background color. The foreground indicates what color the text
- is to become as the cursor passes over it.
- Using a value of -1 for either parameter tells the mouse driver to
- leave that portion of the color alone when the cursor is positioned over a
- character. If you use a color combination of 7, -1 the text under the
- mouse cursor will be shown in white and the background will be unchanged.
- Of course, if both the foreground and background are set to -1, the cursor
- will never be visible.
- A value of -2 causes that color portion to be inverted using an XOR
- process as the cursor moves around the screen. That is, white becomes
- black, green turns to magenta, and blue is translated to brown. Although a
- value of -2 for the background guarantees that the cursor is always
- visible, it can also be distracting to see the mouse cursor color change
- constantly when the screen itself uses many colors. If you want to
- experiment with the various TextColor options, add remarking apostrophes to
- deactivate the three statements after the line IF PEEK(&H463) <> &HB4 THEN
- near the beginning of the program.
- The ShowCursor subprogram simply tells the mouse drive to make the
- mouse cursor visible, in much the same way LOCATE , , 1 option does with
- the normal screen cursor. The companion routine HideCursor turns the mouse
- cursor off again. These are very simple routines that do not require much
- explanation; however, please understand that until you turn the cursor on
- explicitly it remains hidden. As a rule, you also want to ensure that the
- cursor is turned off before you end your program and return to DOS.
- There is one irritating quirk about how the mouse driver keeps track
- of whether the mouse cursor is currently visible or not. When you use the
- statement LOCATE , , 0 to turn off the regular text cursor, the BIOS
- remembers that it is off. And if you subsequently use the same statement
- again the request is ignored. The mouse driver, on the other hand,
- remembers how many times you called HideCursor and requires a corresponding
- number of calls to ShowCursor before it becomes visible. However, the
- reverse is not true. If you turn on the cursor, say, five times in a row,
- only one call to HideCursor is needed to turn it off.
-
-
- READING THE MOUSE BUTTONS AND CURSOR POSITION
-
- The next mouse routine is called ReadCursor, and it calls the service that
- returns both the current mouse cursor position and also which buttons are
- currently pressed. Notice that the X and Y values returned assume graphics
- pixel coordinates even when the display screen is in text mode! Therefore,
- when a monochrome display adapter is being used, the values returned range
- from 0 to 639 horizontally (X), and 0 through 199 vertically (Y). These
- are the same values you would receive when in CGA black and white screen
- mode 2. When in graphics mode, the X and Y values are based on the current
- SCREEN setting. For example, in EGA screen mode 9, the returned value for
- X ranges from 0 through 639, and Y is between 0 and 349.
- When your program is in text mode (SCREEN 0), the current X and Y
- cursor location is based on the upper-left corner of the mouse cursor box.
- Therefore, the actual horizontal range (X) is usually returned between 0
- and 632 to account for a box width of 8 pixels. The vertical location (Y)
- ranges from 0 to 192 for the same reason: If the bottom of the cursor is at
- the bottom of the screen, then the top is eight pixels higher. In graphics
- mode you are allowed to establish any portion of the mouse cursor as being
- the *hot spot*, and this is discussed below in the section "Changing the
- Mouse Cursor Shape".
- The buttons are returned bit coded--the lowest bit is set if button 1
- is pressed, and the next bit is set when the second button is pressed. If
- a mouse has three buttons, the third bit may also be set to indicate that.
- Isolating which bit or combination of bits is set is done using the AND
- logic operator. If Button AND 1 is non-zero then the first button is
- pressed. Similarly, Button AND 2 means the second button is being pressed.
- However, testing for button 3 requires a value of 4, since that is the
- value of the third bit. The program fragment that follows shows this in
- context, and you can press one or more buttons at a time.
-
- DO
- PRINT "Press Ctrl-Break to end."
- CALL ReadCursor(X, Y, Button)
-
- LOCATE 10, 1
- IF Button AND 1 THEN
- PRINT "BUTTON 1"
- ELSE
- PRINT " "
- END IF
-
- LOCATE 10, 11
- IF Button AND 2 THEN
- PRINT "BUTTON 2"
- ELSE
- PRINT " "
- END IF
-
- LOCATE 10, 21
- IF Button AND 4 THEN
- PRINT "BUTTON 3"
- ELSE
- PRINT " "
- END IF
- LOOP
-
- Besides the ReadCursor routine which returns the cursor position and button
- status, I have also included a related function called WaitButton. If your
- program will be waiting for a button and needs to know which button was
- pressed, WaitButton does this using fewer bytes of compiler-generated code.
- Since there are no passed parameters only five bytes are needed to call
- WaitButton, compared to 17 needed to call ReadCursor. WaitButton simply
- waits in an empty loop until a button is pressed, and then reports which
- button it was.
-
-
- CHANGING THE MOUSE CURSOR SHAPE
-
- The CursorShape routine lets you change the size and shape of the mouse
- cursor when the display is in graphics mode. The mouse driver routine that
- is called requires the address of a block of memory 32 words long that
- holds the new shape and color information. The data in this memory block
- is organized into two sections. The first 16 words hold what is called the
- *screen mask*, and the second 16 words hold the *cursor mask*.
- The bits in these masks interact to change the way the foreground and
- background colors on the screen change as the cursor passes over them. The
- method used by the mouse driver to control the cursor shape and colors is
- very complex, and the examples and discussions in Microsoft's documentation
- do little to assist the programmer. Therefore, I have provided a simple
- mechanism that lets you draw the cursor shape using a series of BASIC DATA
- statements.
- Using this method it is easy to control each individual pixel in the
- mouse cursor, and determine if it is white, black, or transparent. When
- the bits in both the screen and cursor masks are both zero, the cursor will
- be black. And when the bits in both masks are set to 1, the color is XORed
- (reversed) at that pixel position. If a screen mask bit is 1 and its
- corresponding bit in the cursor mask is 0, the cursor is transparent.
- Reversing this to make the screen mask 0 and the cursor mask 1 makes the
- cursor white at that position. Thus, you can create nearly any shape for
- the mouse cursor, and a wide variety of interesting color effects.
- If your needs are modest or to minimize the number of DATA statements,
- you can define only the cursor mask and use -1 for the first 16 elements in
- the array by changing that portion of the program like this:
-
-
- DefineCursor:
-
- FOR X = 1 TO 32 'read 32 words of data
- IF X < 17 THEN 'set first 16 elements = -1
- Cursor(X) = -1
- ELSE 'and for the second 16
- READ Dat$ ' read the data and then
- Cursor(X) = Bin2Hex%(Dat$) ' convert to an integer
- END IF
- NEXT
-
- DATA "1100000000000000" 'use only 16 DATA items
- DATA "1110000000000000" ' in this section
- .
- .
-
-
- The other two parameters required by CursorShape are the X and Y cursor hot
- spots. When you call ReadCursor to return the current mouse cursor
- location and button information, the X and Y position returned identifies a
- single pixel on the screen. Which pixel within the mouse cursor that is
- reported is the cursor hot spot. When you use an arrow cursor shape, the
- hot spot is typically the tip of the arrow. This is located in the upper
- left corner of the cursor box and is identified as location 0, 0. However,
- you can also make any other portion of the cursor the hot spot. For
- simplicity, the GOSUB routine at the DefineCursor label always uses 0, 0.
- However, the cross hairs cursor really should use the values 8, 8 to set
- the hot spot at the center of the block.
-
-
- CONTROLLING THE MOUSE CURSOR POSITION AND RANGE
-
- The MoveCursor routine lets you set a new position for the mouse cursor,
- and it too expects pixel values even when the screen is in text mode.
- Although MoveCursor is not demonstrated in this program, it is included in
- the interest of completeness.
- The final mouse subprogram included lets you restrict the range of
- mouse cursor travel, and it is called--appropriately enough--MouseTrap.
- You pass the upper-left and lower-right boundaries to MouseTrap, and it in
- turns passes those values on to the mouse driver. Internally, the mouse
- driver lets you restrict the range for horizontal and vertical motion
- independently. But for simplicity this routines requires both sets of
- values at one time.
- Like the services that ReadCursor and MoveCursor call, these services
- also expect the cursor bounds to be given as pixels even when in text mode.
- Also, notice that the mouse driver always forces the cursor into the
- restricted region for you. That is, if the cursor is in the upper-left
- corner and you call MouseTrap forcing it to stay inside the bottom half of
- the screen, it will be moved to the top of that region.
- Be aware that MouseTrap is also required if you plan to use the 43- or
- 50-line EGA and VGA text modes. By default, the mouse driver assumes that
- a text screen has only 25 lines, and will not normally let the mouse cursor
- be placed below that line. If you have used WIDTH , 50 to put the screen
- into the 50-line mode, the mouse cursor will not be allowed below line 25.
- Therefore, you must use MouseTrap to increase the allowable cursor region
- beyond the default range. Also be aware that using values larger than the
- current screen dimensions let the mouse disappear off the bottom of the
- screen, or wrap around past the right edge and reappear on the left side.
-
-
- ACCESSING THE MOUSE DRIVER
-
- All of the mouse routines considered so far are comprised of a simplified
- interface to the mouse driver through the MouseInt routine. MouseInt lets
- you access any service supported by the mouse driver, including those that
- I have not described here. Similar to the various DOS and BIOS services,
- the mouse driver expects a service number in the AX register. The other
- registers contain the various expected parameters and returned information,
- and they vary from service to service.
- There are no errors returned by the mouse driver, so no mechanism is
- needed to handle errors. For example, if you tell the mouse driver to
- position the cursor off the top edge of the screen, it simply ignores you.
- Unfortunately, discussing every possible mouse service goes beyond
- what I could ever hope to include in a book about BASIC. If you want to
- learn more about the services that are available to you, I recommend
- purchasing a good technical reference such as the Microsoft Mouse
- Programmer's Reference. Other mouse manufacturers also publish their own
- technical manuals, and make them available to the public for a small
- charge. Thankfully, all of the mouse services are consistent across
- brands, although some brands include more features than defined by
- Microsoft. Unless you write programs only for your own use, you should
- avoid relying on services that are specific to a single manufacturer.
-
-
- ACCESSING EXPANDED MEMORY
- =========================
-
- The last set of routines I will present show how you can use interrupts to
- access an expanded memory (EMS) driver. Expanded memory has been available
- for many years, and it provides a way to exceed the normal 640K RAM barrier
- imposed by the 8088 microprocessors. Newer computers that use an 80286 or
- later processors can use what is called Extended Memory (XMS), and this
- type of memory will eventually become the standard way for all computers in
- the future to access more than 1MB of memory. Unfortunately, accessing the
- extended memory beyond 1MB on an 80286-based PC is complicated by a design
- deficiency in that CPU chip. Many people are confused about the difference
- between Expanded and Extended memory, so perhaps a brief explanation is in
- order.
- Extended memory is a single contiguous block that starts at address
- zero and extends through the highest address available, based on the amount
- of memory that is present in a PC. Expanded memory, on the other hand, is
- more complex, and uses a technique called *bank switching*. With bank
- switching, a large amount of memory (up to 16 megabytes) is made available
- to the CPU in 16K blocks. Each of these blocks is called a page, and only
- four of them can be accessed at one time. Thus, the term bank switching is
- appropriate because various banks of far memory are switched in and out of
- a near memory address space.
- The EMS standard requires a 64K contiguous area of near memory within
- the 1MB addressable range to be reserved for use by the EMS driver as a
- *page frame*. On my own PC the 64k address range from &HE000:0000 through
- &HE000:FFFF is not used for any other purpose, and is therefore available
- for use by an EMS driver. At any given time, the four 16K blocks of memory
- within this segment can be connected to memory that lies outside of the 1MB
- normal address range.
- Hardware plug-in EMS boards such as the Intel Above Board contain
- their expanded memory on the board itself. EMS emulator software instead
- converts the Extended memory on computers so equipped to be accessible
- through the 64K segment within the EMS page frame. This is achieved
- through hardware switches that allow any area of memory to be remapped to
- any other range of addresses. In either case, however, Expanded memory is
- made available to an application one page at a time as near memory.
- Each of the four 16K near memory pages in the EMS page frame are
- called *physical pages*, because they reside in physical memory that can be
- accessed directly by the CPU. However, many pages of far EMS memory are
- available--up to four at a time--and these are called *logical pages*.
- This is shown graphically in Figure 11-6.
-
-
- │
- │
- │
- │
- │
- ──────┼────────────┤
- / │ Page 73 │
- 1MB boundary --> ┌────────────┐ / ──────┼────────────┤
- │ ROM BIOS │ / / │ Page 72 │
- ┌─>╞════════════╡/ / ──────┼────────────┤
- │ │ Page 3 │ / / │ │
- │ ├────────────┤/ / │ │
- Physical │ │ Page 2 │ / │ │
- Pages │ ├────────────┼──────────────┼────────────┤
- │ │ Page 1 │ │ Page 45 │
- │ ├────────────┼──────────────┼────────────┤
- │ │ Page 0 │ \ │ │
- └─>├────────────┤\ \────────┼────────────┤
- │ DISPLAY │ \ │ Page 38 │
- │ MEMORY │ \─────────┼────────────┤
- 640K boundary --> ╞════════════╡ │ │
- │ │ │ │
- │ Normal │ │ EMS │
- │ DOS │ │ Logical
- │ Memory Pages │
- │ │
- │ │
- │ └────────────┘
- │
- Address 0 --> └────────────┘
-
- Figure 11-6: How EMS logical pages in far memory are mapped onto physical
- pages in conventional memory.
-
-
- Here, physical page 0 is connected to logical page 38 in expanded memory,
- physical page 1 to logical page 45, and so forth. Whenever a program wants
- to access a particular logical page in expanded memory, it calls the EMS
- driver telling it to map that page to one of the four physical pages in the
- page frame segment. Then, the EMS logical page can be accessed at the near
- memory address within the page frame.
- For simplicity, all of the routines provided here to handle Expanded
- memory use physical page 0 only. Since these routines merely copy array
- data back and forth between conventional and Expanded memory, the data can
- be copied in blocks of 16K and there is no need to have to map multiple
- pages simultaneously. Therefore, these routines always map physical page 0
- to whichever logical page needs to be accessed, and then copy the data in
- that page only.
-
-
- EMS SERVICES
-
- As with the DOS services accessed through Interrupt &H21, the EMS driver
- also uses handles to identify which data you are working with. When memory
- is allocated using EMS Interrupt &H67, you tell the driver how many 16K
- pages you are requesting, and if there is sufficient memory available it
- returns a handle. It should come as no surprise to learn that these
- parameters are passed using the CPU registers. Also like DOS and the BIOS,
- the EMS driver expects a service number in the AH Register. For example,
- the service that requests memory is specified with AH set to &H43.
- To minimize the amount of code that is added to your programs, I have
- created a short assembly language subroutine called EMSInt that replaces
- the Interrupt routine included with BASIC. As with DOSInt and MouseInt,
- this routine lets you pass only the parameters that are actually needed, to
- reduce the amount of compiler-generated code. EMSInt needs access only to
- the AX, BX, CX, and DX registers, so these are the only components in the
- EMSType TYPE structure shown below.
-
- TYPE EMSType
- AX AS INTEGER
- BX AS INTEGER
- CX AS INTEGER
- DX AS INTEGER
- END TYPE
-
- Unlike BASIC's Interrupt routine that has to deal with three parameters and
- code to generate any interrupt number, EMSInt itself is relatively simple:
-
- ;EMSINT.ASM
-
- .Model Medium, Basic
-
- EMSRegs Struc
- RegAX DW ?
- RegBX DW ?
- RegCX DW ?
- RegDX DW ?
- EMSRegs Ends
-
- .Code
-
- EMSInt Proc Uses SI, ERegs:Word
- Mov SI,ERegs ;get the address of EMSRegs
- Mov AX,[SI+RegAX] ;load each register in turn
- Mov BX,[SI+RegBX]
- Mov CX,[SI+RegCX]
- Mov DX,[SI+RegDX]
-
- Int 67h ;call the EMS driver
-
- Mov SI,ERegs ;access EMSRegs again
- Mov [SI+RegAX],AX ;save each register in turn
- Mov [SI+RegBX],BX
- Mov [SI+RegCX],CX
- Mov [SI+RegDX],DX
-
- Ret ;return to BASIC
- EMSInt Endp
- End
-
- If you plan to use the mouse and EMS routines in the same program, you
- could use the MouseRegs variable for both and ignore the Segment portion
- when call EMSInt.
- The program that follows combines a demonstration portion and a
- collection of subprograms and functions. Notice that like the various
- mouse services, you *must* query EMSThere to ensure that an EMS driver is
- loaded before any of the other routines can be used.
-
- 'EMS.BAS, demonstrates the EMS memory services
-
- DEFINT A-Z
-
- DECLARE FUNCTION Compare% (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, _
- BYVAL Adr2, NumBytes)
- DECLARE FUNCTION EMSErrMessage$ (ErrNumber)
- DECLARE FUNCTION EMSError% ()
- DECLARE FUNCTION EMSFree& ()
- DECLARE FUNCTION EMSThere% ()
- DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
-
- DECLARE SUB EMSInt (EMSRegs AS ANY)
- DECLARE SUB EMSStore (Segment, Address, ElSize, NumEls, Handle)
- DECLARE SUB EMSRetrieve (Segment, Address, ElSize, NumEls, Handle)
- DECLARE SUB MemCopy (BYVAL FromSeg, BYVAL FromAdr, BYVAL ToSeg, _
- BYVAL ToAdr, NumBytes)
-
- TYPE EMSType 'similar to DOS Registers
- AX AS INTEGER
- BX AS INTEGER
- CX AS INTEGER
- DX AS INTEGER
- END TYPE
-
- DIM SHARED EMSRegs AS EMSType
- DIM SHARED ErrCode
- DIM SHARED PageFrame
-
-
- CLS
- IF NOT EMSThere% THEN 'ensure EMS is present
- PRINT "No EMS is installed"
- END
- END IF
-
- PRINT "This computer has"; EMSFree&;
- PRINT "kilobytes of EMS available"
-
- REDIM Array#(1 TO 20000)
- FOR X = 1 TO 20000
- Array#(X) = X
- NEXT
-
- CALL EMSStore(VARSEG(Array#(1)), VARPTR(Array#(1)), 8, 20000, Handle)
- IF EMSError% THEN
- PRINT EMSErrMessage$(EMSError%)
- END
- END IF
-
- REDIM Array#(1 TO 20000)
- CALL EMSRetrieve(VARSEG(Array#(1)), VARPTR(Array#(1)), 8, 20000, Handle)
- IF EMSError% THEN
- PRINT EMSErrMessage$(EMSError%)
- END
- END IF
-
- FOR X = 1 TO 20000 'prove it worked
- IF Array#(X) <> X THEN PRINT ".";
- NEXT
- END
-
-
- FUNCTION EMSErrMessage$ (ErrNumber) STATIC
- SELECT CASE ErrNumber
- CASE 128
- EMSErrMessage$ = "Internal error"
- CASE 129
- EMSErrMessage$ = "Hardware malfunction"
- CASE 131
- EMSErrMessage$ = "Invalid handle"
- CASE 133
- EMSErrMessage$ = "No handles available"
- CASE 135, 136
- EMSErrMessage$ = "No pages available"
- CASE ELSE
- IF PageFrame THEN
- EMSErrMessage$ = "Undefined error: " + STR$(ErrNumber)
- ELSE
- EMSErrMessage$ = "EMS not loaded"
- END IF
- END SELECT
- END FUNCTION
-
-
- FUNCTION EMSError% STATIC
- Temp& = ErrCode
- IF Temp& < 0 THEN Temp& = Temp& + 65536
- EMSError% = Temp& \ 256
- END FUNCTION
-
-
- FUNCTION EMSFree& STATIC
- EMSFree& = 0 'assume failure
- IF PageFrame = 0 THEN EXIT FUNCTION
-
- EMSRegs.AX = &H4200
- CALL EMSInt(EMSRegs)
- ErrCode = EMSRegs.AX 'save possible error from AH
-
- IF ErrCode = 0 THEN EMSFree& = EMSRegs.BX * 16
- END FUNCTION
-
-
- SUB EMSRetrieve (Segment, Address, ElSize, NumEls, Handle) STATIC
- IF PageFrame = 0 THEN EXIT SUB
-
- LocalSeg& = Segment 'use copies we can change
- LocalAdr& = Address
-
- BytesNeeded& = NumEls * CLNG(ElSize)
- PagesNeeded = BytesNeeded& \ 16384
- Remainder = BytesNeeded& MOD 16384
- IF Remainder THEN PagesNeeded = PagesNeeded + 1
-
- NumBytes = 16384 'assume we're copying a
- ' complete page
- ThisPage = 0 'start copying to page 0
-
- FOR X = 1 TO PagesNeeded 'copy the data
- IF X = PagesNeeded THEN 'watch out for last page
- IF Remainder THEN NumBytes = Remainder
- END IF
-
- IF LocalAdr& > 32767 THEN 'handle segment boundaries
- LocalAdr& = LocalAdr& - &H8000&
- LocalSeg& = LocalSeg& + &H800
- IF LocalSeg& > 32767 THEN
- LocalSeg& = LocalSeg& - 65536
- END IF
- END IF
-
- EMSRegs.AX = &H4400 'map physical page 0 to the
- EMSRegs.BX = ThisPage ' current logical page
- EMSRegs.DX = Handle ' for the given handle
- CALL EMSInt(EMSRegs) 'then copy the data there
- ErrCode = EMSRegs.AX 'save possible error from AH
- IF ErrCode THEN EXIT SUB
- CALL MemCopy(PageFrame, Zero, CINT(LocalSeg&), CINT(LocalAdr&), _
- NumBytes)
-
- ThisPage = ThisPage + 1
- LocalAdr& = LocalAdr& + NumBytes
- NEXT
-
- EMSRegs.AX = &H4500 'release memory service
- EMSRegs.DX = Handle
- CALL EMSInt(EMSRegs)
- ErrCode = EMSRegs.AX 'save possible error
- END SUB
-
-
- SUB EMSStore (Segment, Address, ElSize, NumEls, Handle) STATIC
-
- IF PageFrame = 0 THEN EXIT SUB
-
- LocalSeg& = Segment 'use copies we can change
- LocalAdr& = Address
-
- BytesNeeded& = NumEls * CLNG(ElSize)
- PagesNeeded = BytesNeeded& \ 16384
- Remainder = BytesNeeded& MOD 16384
- IF Remainder THEN PagesNeeded = PagesNeeded + 1
-
- EMSRegs.AX = &H4300 'allocate memory service
- EMSRegs.BX = PagesNeeded
- CALL EMSInt(EMSRegs)
-
- ErrCode = EMSRegs.AX 'save possible error from AH
- IF ErrCode THEN EXIT SUB
- Handle = EMSRegs.DX 'save the handle returned
-
- NumBytes = 16384 'assume we're copying a
- ' complete page
- ThisPage = 0 'start copying to page 0
-
- FOR X = 1 TO PagesNeeded 'copy the data
- IF X = PagesNeeded THEN 'watch out for last page
- IF Remainder THEN NumBytes = Remainder
- END IF
-
- IF LocalAdr& > 32767 THEN 'handle segment boundaries
- LocalAdr& = LocalAdr& - &H8000&
- LocalSeg& = LocalSeg& + &H800
- IF LocalSeg& > 32767 THEN
- LocalSeg& = LocalSeg& - 65536
- END IF
- END IF
-
- EMSRegs.AX = &H4400 'map physical page 0 to the
- EMSRegs.BX = ThisPage ' current logical page
- EMSRegs.DX = Handle ' for the given handle
- CALL EMSInt(EMSRegs) 'then copy the data there
- ErrCode = EMSRegs.AX 'save possible error from AH
- IF ErrCode THEN EXIT SUB
- CALL MemCopy(CINT(LocalSeg&), CINT(LocalAdr&), PageFrame, Zero, _
- NumBytes)
-
- ThisPage = ThisPage + 1
- LocalAdr& = LocalAdr& + NumBytes
- NEXT
- END SUB
-
-
- FUNCTION EMSThere% STATIC
- EMSThere% = 0 'assume the worst
- DIM DevName AS STRING * 8
- DevName = "EMMXXXX0" 'search for this below
-
- '---- Try to find the string "EMMXXXX0" at offset 10 in the EMS handler.
- ' If it's not there then EMS cannot possibly be installed.
- Int67Seg = PeekWord%(0, (&H67 * 4) + 2)
- IF NOT Compare%(Int67Seg, 10, VARSEG(DevName$), VARPTR(DevName$), 8) THEN
- EXIT FUNCTION
- END IF
-
- EMSRegs.AX = &H4100 'get Page Frame Segment service
- CALL EMSInt(EMSRegs)
- ErrCode = EMSRegs.AX 'save possible error from AH
-
- IF ErrCode = 0 THEN
- EMSThere% = -1
- PageFrame = EMSRegs.BX
- END IF
- END FUNCTION
-
- EMS.BAS begins by declaring all of the subprograms and functions that it
- uses, as well as the EMSType structure. The three shared variables are
- used by the various procedures, and should not be removed when you delete
- the demo portion to create a reusable module.
-
-
- DETERMINING IF EMS IS PRESENT
-
- The first function used is EMSThere, which reports if an EMS driver is
- loaded and operative. EMSThere begins by assuming that an EMS driver is
- not loaded, and assigns a function output value of 0. Then it attempts to
- find the device name "EMMXXXX0" in the header portion of the EMS device
- driver. Like the MouseThere function that checked the interrupt vector
- table for a non-zero segment value, this preliminary check is also needed
- to prevent a system lockup on older computers running DOS version 2.
- To search for this string EMSThere uses PeekWord to retrieve the
- segment for Interrupt &H67, and then looks at the eight bytes at offset 10
- within that segment. If the Compare function finds the unique identifying
- string, it knows that the driver is loaded and it is safe to invoke
- Interrupt &H67. Service &H41 returns either -1 in AX if the driver is
- active, or 0 if it is not. This service also returns the page frame
- segment the driver is using in near memory, and EMSThere saves this value
- in the shared variable PageFrame for access by the other routines.
-
-
- DETERMINING AVAILABLE EMS MEMORY
-
- The second function, EMSFree, returns the number of 16K EMS pages that are
- available to your program. The remainder of the demonstration simply
- dimensions a 20,000 element double precision array, and then saves it to
- expanded memory. Because this array exceeds 64K, you must start BASIC with
- the /ah command line switch. Otherwise you will receive a "Subscript out
- of range" error message.
- EMSFree uses function &H42 to ask the EMS driver for the number of
- free pages, and the driver returns the page count in BX. Although it is
- not shown here, service &H42 also returns the total number of pages in the
- DX register. Therefore, you could easily create a TotalPages function from
- a copy of EMSFree by changing the line that assigns the function output to
- instead be IF ErrCode = 0 THEN TotalPages& = EMSRegs.DX * 16.
-
-
- STORING AND RETRIEVING DATA
-
- The actual storing and retrieving of data to and from Expanded memory is
- fairly complicated, because of the need to map different logical pages to
- physical page zero. Although Figure 11-6 shows a single group of logical
- pages, the EMS driver really maintains a separate series of logical pages
- for each active handle.
- EMSStore and EMSRetrieve store and retrieve data in Expanded memory
- respectively, and both of these subprograms are designed to accommodate
- huge arrays larger than 64k. Therefore, additional work is needed to
- calculate new segment values as each 16K portion has been processed.
- As with all of the EMS procedures shown here, EMSStore begins by
- verifying that EMSThere has already been invoked, and that a valid page
- frame segment has been obtained. The next step is to make long integer
- copies of the incoming segment and address parameters. Because of the
- segment arithmetic that is performed later in the routine, long integers
- are needed to allow values greater than 32,767 to be compared. Equally
- important, a routine should never alter incoming parameters unless they
- also return information or such changes are expected.
- Next, EMSStore determines the total number of bytes of EMS storage
- that are needed, and from that calculates the total number of 16K pages.
- Because the EMS driver allocates entire pages only, an odd number of bytes
- requires an entire additional page. BASIC's MOD function is used for this,
- and if the result is non-zero, the TotalPages variable is incremented.
- Once the number of pages is known, service &H43 is called to allocate
- the Expanded memory. The remainder of the procedure walks through the
- array data in 16K increments, mapping physical page zero to the next
- logical page in sequence. Note the code that tests the current address to
- see if it is within 32K of spanning a segment boundary. In that case, the
- address is dropped by 32K, and the segment is increased by an equivalent
- amount. Because each new segment starts 16 bytes higher than the previous
- one, 32K \ 16 is added to LocalSeg& rather than a full 32K.
- After the array is stored in EMS, it is redimensioned in the
- demonstration and then retrieved using the EMSRetrieve subprogram.
- EMSRetrieve is nearly identical to EMSStore, except it copies from EMS to
- the array, and releases memory when it is finished rather than claim it at
- the beginning. The final step in the demonstration is to examine the value
- in each element, to prove that the array was restored correctly.
-
-
- DETECTING EMS ERRORS
-
- The EMSError function retrieves the current value of ErrCode, and
- manipulates it into a form useable by your programs. EMS errors are
- returned in the AH register, which requires dividing by 256 to derive a
- single byte value. But since EMS error numbers start at 128, the value
- returned in AX appears negative to BASIC programs which treat all integers
- as being signed. This is why a long integer is used initially and then
- converted to a positive value, before dividing to produce the final result.
- The EMSErrMessage function can be used to display an appropriate
- message if an error is detected. The incoming error code is filtered
- through a series of CASE statements, based on the error values defined by
- the EMS specification.
-
-
- SUGGESTED ENHANCEMENTS
-
- The routines presented herein provide a limited set of services for
- accessing Expanded memory. However, there are several improvements you can
- make, and a few other uses that I have not shown. If you are using BASIC
- PDS [or VB/DOS], one useful enhancement you can add is to change the
- subprograms and functions to receive their parameters by value using the
- BYVAL option. In fact, this can also be done with the DOS and mouse
- routines, to minimize the amount of code the BASIC compiler adds to your
- final executable program.
- Although this demonstration shows storing array data only, you can
- also use these routines to store and retrieve text and graphics screens.
- This is much quicker than saving them to disk, as was shown in Chapter 6.
- For example, to save a 25 line by 80 column color text screen in Expanded
- memory you would use the appropriate segment and address like this:
-
- CALL EMSStore(&HB800, 0, 1, 4000, Handle)
- CALL EMSRetrieve(&HB800, 0, 1, 4000, Handle)
-
- Just as you can cause problems by failing to close DOS handles during the
- development of a program, the same problem can happen with an EMS driver.
- Unfortunately, it is not as easy to know which handle numbers are still
- open if you have not kept track of them yourself manually. DOS issues its
- handles using a sensible series of sequential numbers. This is not
- necessarily the case with EMS handles. The EMM386.EXE driver provided by
- Microsoft does issue sequential handles, starting with handle 1. But many
- drivers use other starting values, some work from high numbers backwards,
- and yet others use a handle number sequence that is not in order.
- Finally, to learn about all of the possible EMS services you need a
- good reference. Although the primary services are shown here, there are
- several others you may find useful. For example, service &H46 lets you
- retrieve the EMS version number, and service &H4C lets you see how many
- pages are currently allocated for a given handle. The EMS driver version
- can be valuable, because newer drivers offer more features which you may
- want to take advantage of. Ray Duncan's book "Advanced MS-DOS" mentioned
- earlier is one good source, and it lists each EMS service and the possible
- errors that can be returned.
-
-
- SUMMARY
- =======
-
- In this chapter you learned how BASIC--and indeed, all languages--use
- interrupts to communicate with the operating system. You learned what
- interrupts are and how to access them, and how the CPU registers are used
- to communicate information between your program and the interrupt handler
- being invoked. You also learned how some of the two-byte registers can be
- treated as two one-byte registers, which requires multiplying and dividing
- to access those portions individually.
- A number of complete programs were presented showing how to access the
- BIOS, DOS, the mouse driver, and Expanded memory. In the section on BIOS
- interrupts, examples were given that showed how to simulate pressing the
- PrtSc key, and also how to call the video service that clears or scrolls
- only a portion of the display screen.
- The DOS examples included a complete set of subroutines to replace
- BASIC's file handling statements. One advantage gained by bypassing BASIC
- is to read and write large amounts of data at one time. Another is to
- avoid the need for ON ERROR in certain programming situations. Although
- calling the DOS services directly can be beneficial in many cases, it also
- requires more work on your part. However, some services cannot be accessed
- using BASIC alone, such as reading file and directory names, or determining
- a file's attribute. Where BASIC employs string descriptors to know how
- long a string is, DOS instead uses a CHR$(0) zero byte to mark the end.
- The mouse and Expanded memory discussions described how those
- interrupt services are accessed, and provided practical advice and warnings
- where appropriate. Although a large number of interrupt routines were
- described, there is a practical limit to how much information can be
- provided here. In particular, you will need a separate reference manual
- that describes the details of each interrupt service routine in depth.
- In the next and final chapter you will learn how to program in
- assembly language, and how to add assembly language routines to programs
- you write using BASIC. Assembly language is unlike any high-level
- language, and it provides the ultimate means to exploit fully all of the
- resources in a PC.